深入理解volatile
生活随笔
收集整理的這篇文章主要介紹了
深入理解volatile
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
在理解volatile之前,我們先來看下CPU的工作模式:
處理器這種工作產(chǎn)生的問題:1、所有的變量在處理器運(yùn)算期間都是變量對(duì)應(yīng)值的一個(gè)副本,其它處理器無法感知其對(duì)變量的操作。2、處理器為了高效利用寄存器而對(duì)指令的重排在多線程下將會(huì)產(chǎn)生無法預(yù)測(cè)的結(jié)果。3、不同的處理器針對(duì)同一套編碼所產(chǎn)生的指令會(huì)有不同的運(yùn)行策略。
為了解決上述三個(gè)問題JVM為了保證每個(gè)平臺(tái)代碼運(yùn)行結(jié)果的一致性提出了JMM(JAVA內(nèi)存模型),目的是為了讓Java程序在各種平臺(tái)下都能達(dá)到一致性的結(jié)果。
JMM規(guī)范:Happen-Before原則:1、程序順序原則:一個(gè)線程內(nèi)保證語意的串行化2、volatile規(guī)則:volatile變量的寫先發(fā)生于讀,這保證了volatile變量的可見性3、鎖規(guī)則:解鎖必然發(fā)生于加鎖前4、傳遞性:A先于B,B先于C,A一定先于C5、線程的start()方法先于它的每一個(gè)動(dòng)作6、線程的所有動(dòng)作,先于線程的終結(jié)7、線程的中斷先于被中斷的代碼8、對(duì)象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于finalize()方法
針對(duì)volatile的優(yōu)化:volatile能保證修改對(duì)其它線程可見。即修改了共享變量后肯定會(huì)刷回主內(nèi)存,通知其它線程,但是為了使處理器的內(nèi)部單元高效工作,處理器會(huì)對(duì)輸入的代碼進(jìn)行亂序即指令重排。對(duì)于volatile如果不做針對(duì)性的處理,那顯然volatile的可見性并不會(huì)有什么意義。并不能保證結(jié)果的確定性。針對(duì)volatileJVM做了大量的工作:關(guān)于工作內(nèi)存(針對(duì)硬件就是高速緩存)JMM定義了8種操作來完成:
針對(duì)于volatile變量又有額外如下定義:volatile變量在use時(shí),必須執(zhí)行l(wèi)oad操作。即每次使用volatile變量必須先從主內(nèi)存中刷新最新值。 volatile變量在assign時(shí),必須執(zhí)行write操作。即每次對(duì)volatile進(jìn)行賦值操作必須立馬同步回主內(nèi)存。 針對(duì)volatile和普通變量,或者volatile變量和volatile變量一起使用時(shí)。
JVM在編譯期間也會(huì)針對(duì)volatile的重排加以干涉,干涉規(guī)則如下:如果第二個(gè)操作時(shí)volatile寫操作,不管第一操作是什么操作,都不能重排。 如果第一個(gè)操作時(shí)volatile讀操作,不管第二個(gè)操作時(shí)什么操作,都不能重排。 volatile寫和volatile讀不能重排。
為了實(shí)現(xiàn)這個(gè)語意,JVM在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障(memory barrier)來禁止特定類型的處理器指令重排,對(duì)于編譯器來說對(duì)所有的CPU來插入屏障數(shù)最小的方案幾乎不可能,下面是基于保守策略的JMM內(nèi)存屏障插入策略:在每個(gè)volatile寫操作前面插入StoreStore屏障 在每個(gè)volatile寫操作后插入StoreLoad屏障 在每個(gè)volatile讀后面插入一個(gè)LoadLoad屏障 在每個(gè)volatile讀后面插入一個(gè)LoadStore屏障
這里要說下內(nèi)存屏障是是什么東西:硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障,內(nèi)存屏障的作用有兩個(gè):
LoadLoad,StoreStore,LoadStore,StoreLoad實(shí)際上是Java對(duì)上面兩種屏障的組合,來完成一系列的屏障和數(shù)據(jù)同步功能:
即使JMM對(duì)volatile做了這么多的工作,它也僅僅只保證了volatile變量在原子性操作下多個(gè)線程之間的正確同步,對(duì)非原子操作,使用volatile仍然會(huì)發(fā)生無法預(yù)知的結(jié)果。比如對(duì)i++操作,在多線程情況下結(jié)果依然是不定:例子:
我們來使用 javap -c 來看下這個(gè)文件的編譯指令:
根據(jù)volatile的內(nèi)存語意我們可以總結(jié)出兩條安全使用volatile的方式:
處理器這種工作產(chǎn)生的問題:1、所有的變量在處理器運(yùn)算期間都是變量對(duì)應(yīng)值的一個(gè)副本,其它處理器無法感知其對(duì)變量的操作。2、處理器為了高效利用寄存器而對(duì)指令的重排在多線程下將會(huì)產(chǎn)生無法預(yù)測(cè)的結(jié)果。3、不同的處理器針對(duì)同一套編碼所產(chǎn)生的指令會(huì)有不同的運(yùn)行策略。
為了解決上述三個(gè)問題JVM為了保證每個(gè)平臺(tái)代碼運(yùn)行結(jié)果的一致性提出了JMM(JAVA內(nèi)存模型),目的是為了讓Java程序在各種平臺(tái)下都能達(dá)到一致性的結(jié)果。
JMM規(guī)范:Happen-Before原則:1、程序順序原則:一個(gè)線程內(nèi)保證語意的串行化2、volatile規(guī)則:volatile變量的寫先發(fā)生于讀,這保證了volatile變量的可見性3、鎖規(guī)則:解鎖必然發(fā)生于加鎖前4、傳遞性:A先于B,B先于C,A一定先于C5、線程的start()方法先于它的每一個(gè)動(dòng)作6、線程的所有動(dòng)作,先于線程的終結(jié)7、線程的中斷先于被中斷的代碼8、對(duì)象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于finalize()方法
針對(duì)volatile的優(yōu)化:volatile能保證修改對(duì)其它線程可見。即修改了共享變量后肯定會(huì)刷回主內(nèi)存,通知其它線程,但是為了使處理器的內(nèi)部單元高效工作,處理器會(huì)對(duì)輸入的代碼進(jìn)行亂序即指令重排。對(duì)于volatile如果不做針對(duì)性的處理,那顯然volatile的可見性并不會(huì)有什么意義。并不能保證結(jié)果的確定性。針對(duì)volatileJVM做了大量的工作:關(guān)于工作內(nèi)存(針對(duì)硬件就是高速緩存)JMM定義了8種操作來完成:
- lock(加鎖): 作用于主內(nèi)存,把一個(gè)變量標(biāo)記為線程獨(dú)占。
- unlock(解鎖):作用于主內(nèi)存,把一個(gè)已鎖定的變量釋放出來。
- read(讀取):作用于主內(nèi)存,將一個(gè)變量從主內(nèi)從中傳輸?shù)焦ぷ鲀?nèi)存中,以便隨后的load。
- load(載入):作用于工作內(nèi)存,把read操作得到的變量放在工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存,把工作內(nèi)存中的一個(gè)變量傳遞給執(zhí)行引擎。
- assign(賦值):作用于工作內(nèi)存,把一個(gè)執(zhí)行引擎接受的值賦值給工作內(nèi)存的變量。
- store(存儲(chǔ)):作用于工作內(nèi)存,把工作內(nèi)存中的一個(gè)變量的值傳輸?shù)街鲀?nèi)存,以便后續(xù)的write操作。
- write(寫入):作用于主內(nèi)存,把store操作從工作內(nèi)存得到的值放回主內(nèi)存中。
- 不允許load和read,store和write單獨(dú)出現(xiàn)。
- 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變,必須同步回主內(nèi)存。
- 不與許一個(gè)線程無原因的(沒有assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存。
- 一個(gè)新的變量只能在主內(nèi)存中誕生。
- 一個(gè)變量只能同時(shí)有一個(gè)線程進(jìn)行加鎖。lock可以被同一個(gè)線程加鎖多次,但是必須解鎖相同次數(shù)。這個(gè)變量才會(huì)被解鎖。
- 對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作。將會(huì)先清空該線程的工作內(nèi)存中的該變量的值。在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作。
- 一個(gè)變量被lock,不允許其它線程執(zhí)行unlock。也不允許執(zhí)行unlock被別的線程lock的變量。即一個(gè)線程自己lock的只有自己能unlock.
- 一個(gè)變量unlock之前,工作內(nèi)存中的數(shù)據(jù)必須同步回主內(nèi)存。
針對(duì)于volatile變量又有額外如下定義:
JVM在編譯期間也會(huì)針對(duì)volatile的重排加以干涉,干涉規(guī)則如下:
為了實(shí)現(xiàn)這個(gè)語意,JVM在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障(memory barrier)來禁止特定類型的處理器指令重排,對(duì)于編譯器來說對(duì)所有的CPU來插入屏障數(shù)最小的方案幾乎不可能,下面是基于保守策略的JMM內(nèi)存屏障插入策略:
這里要說下內(nèi)存屏障是是什么東西:硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障,內(nèi)存屏障的作用有兩個(gè):
- 阻止屏障兩側(cè)的的指令重排
- 強(qiáng)制把高速緩存中的數(shù)據(jù)更新或者寫入到主存中。Load Barrier負(fù)責(zé)更新高速緩存, Store Barrier負(fù)責(zé)將高速緩沖區(qū)的內(nèi)容寫回主存
LoadLoad,StoreStore,LoadStore,StoreLoad實(shí)際上是Java對(duì)上面兩種屏障的組合,來完成一系列的屏障和數(shù)據(jù)同步功能:
- LoadLoad屏障:對(duì)于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore屏障:對(duì)于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見。
- LoadStore屏障:對(duì)于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreLoad屏障:對(duì)于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見。它的開銷是四種屏障中最大的。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障是個(gè)萬能屏障,兼具其它三種內(nèi)存屏障的功能。
- StoreStore屏障可以保證在volatile寫之前,所有的普通寫操作已經(jīng)對(duì)所有處理器可見,StoreStore屏障保障了在volatile寫之前所有的普通寫操作已經(jīng)刷新到主存。
- StoreLoad屏障避免volatile寫與下面有可能出現(xiàn)的volatile讀/寫操作重排。因?yàn)榫幾g器無法準(zhǔn)確判斷一個(gè)volatile寫后面是否需要插入一個(gè)StoreLoad屏障(寫之后直接就return了,這時(shí)其實(shí)沒必要加StoreLoad屏障),為了能實(shí)現(xiàn)volatile的正確內(nèi)存語意,JVM采取了保守的策略。在每個(gè)volatile寫之后或每個(gè)volatile讀之前加上一個(gè)StoreLoad屏障,而大多數(shù)場(chǎng)景是一個(gè)線程寫volatile變量多個(gè)線程去讀volatile變量,同一時(shí)刻讀的線程數(shù)量其實(shí)遠(yuǎn)大于寫的線程數(shù)量。選擇在volatile寫后面加入StoreLoad屏障將大大提升執(zhí)行效率(上面已經(jīng)說了StoreLoad屏障的開銷是很大的)。
- LoadLoad屏障保證了volatile讀不會(huì)與下面的普通讀發(fā)生重排
- LoadStore屏障保證了volatile讀不回與下面的普通寫發(fā)生重排。
即使JMM對(duì)volatile做了這么多的工作,它也僅僅只保證了volatile變量在原子性操作下多個(gè)線程之間的正確同步,對(duì)非原子操作,使用volatile仍然會(huì)發(fā)生無法預(yù)知的結(jié)果。比如對(duì)i++操作,在多線程情況下結(jié)果依然是不定:例子:
我們來使用 javap -c 來看下這個(gè)文件的編譯指令:
根據(jù)volatile的內(nèi)存語意我們可以總結(jié)出兩條安全使用volatile的方式:
- 運(yùn)算結(jié)果不依賴于volatile變量的當(dāng)前值,或者能保證只有單一線程能修改變量的值
- 變量不需要與其它的狀態(tài)變量共同參與不變性。
轉(zhuǎn)載于:https://juejin.im/post/5bb4a26fe51d450e7b174c16
總結(jié)
以上是生活随笔為你收集整理的深入理解volatile的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软重新开源 MS-DOS 1.25/2
- 下一篇: SDUT-2132_数据结构实验之栈与队