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