您好,登錄后才能下訂單哦!
在談及線程安全時(shí),常會(huì)說(shuō)到一個(gè)變量——volatile。在《Java并發(fā)編程實(shí)戰(zhàn)》一書(shū)中是這么定義volatile的——“Java語(yǔ)言提供了一種稍弱的同步機(jī)制,即volatile變量,用來(lái)確保將變量的更新操作通知到其他線程”。這句話說(shuō)明了兩點(diǎn):①volatile變量是一種同步機(jī)制;②volatile能夠確??梢?jiàn)性。這兩點(diǎn)和我們探討“volatile變量是否能夠保證線程安全性”息息相關(guān)。
什么是同步機(jī)制?在并發(fā)程序設(shè)計(jì)中,各進(jìn)程對(duì)公共變量的訪問(wèn)必須加以制約,這種制約稱為同步。也就是說(shuō),同步機(jī)制即為對(duì)共享資源的一種制約。那么問(wèn)題來(lái)了:volatile這種“稍弱的同步機(jī)制”是怎么制約各個(gè)進(jìn)程對(duì)共享資源的訪問(wèn)的呢?答案就在“volatile能夠確??梢?jiàn)性”中。
2.1 可見(jiàn)性
volatile能夠保證字段的可見(jiàn)性:volatile變量,用來(lái)確保將變量的更新操作通知到其他線程。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見(jiàn)的地方,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新寫(xiě)入的值。
可見(jiàn)性和“線程如何對(duì)變量進(jìn)行操作(取值、賦值等)”有關(guān)系:
我們要先明確一個(gè)定律:線程對(duì)變量的所有操作(取值、賦值等)都必須在工作內(nèi)存(各線程獨(dú)立擁有)中進(jìn)行,而不能直接讀寫(xiě)內(nèi)存中的變量,各工作內(nèi)存間也不能相互訪問(wèn)。對(duì)于volatile變量來(lái)說(shuō),由于它特殊的操作順序性規(guī)定,看起來(lái)如同操作主內(nèi)存一般,但實(shí)際上 volatile變量也是遵循這一定律的。
關(guān)于主存與工作內(nèi)存之間具體的交互協(xié)議(即一個(gè)變量如何從主存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主存等實(shí)現(xiàn)細(xì)節(jié)),Java內(nèi)存模型中定義了以下八種操作來(lái)完成:
lock:(鎖定),unlock(解鎖),read(讀取),load(載入),use(試用), assign(賦值),store(存儲(chǔ)),write(寫(xiě)入)。
volatile 對(duì)這八種操作有著兩個(gè)特殊的限定,正因?yàn)橛羞@些限定才讓volatile修飾的變量有可見(jiàn)性以及可以禁止指令重排序 :
① use動(dòng)作之前必須要有read和load動(dòng)作, 這三個(gè)動(dòng)作必須是連續(xù)出現(xiàn)的?!颈硎荆好看喂ぷ鲀?nèi)存要使用volatile變量之前必須去主存中拿取最新的volatile變量】
② assign動(dòng)作之后必須跟著store和write動(dòng)作,這三個(gè)動(dòng)作必須是連續(xù)出現(xiàn)的?!颈硎? 每次工作內(nèi)存改變了volatile變量的值,就必須把該值寫(xiě)回到主存中】
有以上兩條規(guī)則就能保證每個(gè)線程每次去拿volatile變量的時(shí)候,那個(gè)變量肯定是最新的, 其實(shí)也就相當(dāng)于好多個(gè)線程用的是同一個(gè)內(nèi)存,無(wú)工作內(nèi)存和主存之分。而操作沒(méi)有用volatile修飾的變量則不能保證每次都能獲取到最新的變量值。
2.2 所以volatile究竟能否保證線程安全性?
不能。
通過(guò)2.1,我們已經(jīng)很明確在多線程環(huán)境下,一個(gè)線程修改了用volatile修飾的變量后,其他線程能夠立刻讀取到該變量的最新值。但是,volatile并不能保證各個(gè)線程是串行去訪問(wèn)同一變量的,在機(jī)器是多核的情況下,兩個(gè)或多個(gè)線程同時(shí)對(duì)同一共享變量做修改,依舊會(huì)出現(xiàn)線程安全問(wèn)題。例如:機(jī)器是多核的情況下,i == 0,兩個(gè)線程同時(shí)操作 i++,最終的結(jié)果就有可能出現(xiàn)錯(cuò)誤的結(jié)果“ i == 1”。
所以:volatile不能保證線程安全性,因?yàn)橐WC線程安全性就得保證是以串行形式來(lái)訪問(wèn)操作共享資源的,而volatile做不到這點(diǎn)。
2.3 通過(guò)代碼來(lái)驗(yàn)證“即使變量用了volatile來(lái)修飾,依舊會(huì)出現(xiàn)線程安全問(wèn)題”
測(cè)試結(jié)果:(1)出現(xiàn)了大量的重復(fù)數(shù)字; (2)最后還輸出了 “-1”;==》說(shuō)明變量即使用volatile修飾了但依舊出現(xiàn)了線程安全問(wèn)題。
代碼解析:
出現(xiàn)問(wèn)題(1)的原因:線程存在“先檢查后執(zhí)行”的競(jìng)態(tài)條件??赡苡袃蓚€(gè)線程同時(shí)擁有CPU的執(zhí)行權(quán)(機(jī)器是雙核的),它們判斷到做“if (ticket > 0)”,并同時(shí)做“ticket--”操作。
出現(xiàn)問(wèn)題(2)的原因:
①當(dāng)ticket==1時(shí),兩個(gè)或多個(gè)線程同時(shí)通過(guò)了“if (ticket > 0)”的判斷,并進(jìn)入了判斷框中去執(zhí)行代碼;
②然后它們執(zhí)行到“Thread.sleep(100);”就睡了;
③睡醒后總有一個(gè)線程會(huì)先搶到cup的執(zhí)行權(quán),然后執(zhí)行“ticket--”操作,并將最新的ticket數(shù)值推送告知到每個(gè)線程;
④此時(shí)那些在判斷框中的其他的線程并不會(huì)再次做“if (ticket > 0)”的判斷,而是直接拿最新的ticket并做“ticket--”操作。
就算線程在“ticket--”之前每次都做“if (ticket > 0)”的判斷,也依舊會(huì)有線程安全問(wèn)題,因?yàn)橛挚赡艹霈F(xiàn)①那種同時(shí)通過(guò)判斷的狀態(tài)。
總結(jié):volatile只能確??梢?jiàn)性和防止字段重排序(防止字段重排序本文中沒(méi)做深入討論),不能保證線程安全性。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。