深刻理解volatile

在理解volatile以前,咱們先來看下CPU的工做模式:



處理器這種工做產生的問題:
一、全部的變量在處理器運算期間都是變量對應值的一個副本,其它處理器沒法感知其對變量的操做。
二、處理器爲了高效利用寄存器而對指令的重排在多線程下將會產生沒法預測的結果。
三、不一樣的處理器針對同一套編碼所產生的指令會有不一樣的運行策略。

爲了解決上述三個問題JVM爲了保證每一個平臺代碼運行結果的一致性提出了JMM(JAVA內存模型),目的是爲了讓Java程序在各類平臺下都能達到一致性的結果。

JMM規範:
Happen-Before原則:
一、程序順序原則:一個線程內保證語意的串行化
二、volatile規則:volatile變量的寫先發生於讀,這保證了volatile變量的可見性
三、鎖規則:解鎖必然發生於加鎖前
四、傳遞性:A先於B,B先於C,A必定先於C
五、線程的start()方法先於它的每個動做
六、線程的全部動做,先於線程的終結
七、線程的中斷先於被中斷的代碼
八、對象的構造函數執行、結束先於finalize()方法

針對volatile的優化:
volatile能保證修改對其它線程可見。即修改了共享變量後確定會刷回主內存,通知其它線程,可是爲了使處理器的內部單元高效工做,處理器會對輸入的代碼進行亂序即指令重排。對於volatile若是不作針對性的處理,那顯然volatile的可見性並不會有什麼意義。並不能保證結果的肯定性。
針對volatileJVM作了大量的工做:
關於工做內存(針對硬件就是高速緩存)JMM定義了8種操做來完成:
  • lock(加鎖): 做用於主內存,把一個變量標記爲線程獨佔。
  • unlock(解鎖):做用於主內存,把一個已鎖定的變量釋放出來。
  • read(讀取):做用於主內存,將一個變量從主內從中傳輸到工做內存中,以便隨後的load。
  • load(載入):做用於工做內存,把read操做獲得的變量放在工做內存的變量副本中。
  • use(使用):做用於工做內存,把工做內存中的一個變量傳遞給執行引擎。
  • assign(賦值):做用於工做內存,把一個執行引擎接受的值賦值給工做內存的變量。
  • store(存儲):做用於工做內存,把工做內存中的一個變量的值傳輸到主內存,以便後續的write操做。
  • write(寫入):做用於主內存,把store操做從工做內存獲得的值放回主內存中。
8中操做有以下關係:
  • 不容許load和read,store和write單獨出現。
  • 不容許一個線程丟棄它最近的assign操做,即變量在工做內存中改變,必須同步回主內存。
  • 不與許一個線程無緣由的(沒有assign操做)把數據從工做內存同步回主內存。
  • 一個新的變量只能在主內存中誕生。
  • 一個變量只能同時有一個線程進行加鎖。lock能夠被同一個線程加鎖屢次,可是必須解鎖相同次數。這個變量纔會被解鎖。
  • 對一個變量執行lock操做。將會先清空該線程的工做內存中的該變量的值。在執行引擎使用這個變量前,須要從新執行load或assign操做。
  • 一個變量被lock,不容許其它線程執行unlock。也不容許執行unlock被別的線程lock的變量。即一個線程本身lock的只有本身能unlock.
  • 一個變量unlock以前,工做內存中的數據必須同步回主內存。
這八種操做和其使用規則,決定了變量在工做內存和主內存之間的同步策略。

針對於volatile變量又有額外以下定義:
  1. volatile變量在use時,必須執行load操做。即每次使用volatile變量必須先從主內存中刷新最新值。
  2. volatile變量在assign時,必須執行write操做。即每次對volatile進行賦值操做必須立馬同步回主內存。
針對volatile和普通變量,或者volatile變量和volatile變量一塊兒使用時。

JVM在編譯期間也會針對volatile的重排加以干涉,干涉規則以下:


  1. 若是第二個操做時volatile寫操做,無論第一操做是什麼操做,都不能重排。
  2. 若是第一個操做時volatile讀操做,無論第二個操做時什麼操做,都不能重排。
  3. volatile寫和volatile讀不能重排。

爲了實現這個語意,JVM在生成字節碼時,會在指令序列中插入內存屏障(memory barrier)來禁止特定類型的處理器指令重排,對於編譯器來講對全部的CPU來插入屏障數最小的方案几乎不可能,下面是基於保守策略的JMM內存屏障插入策略:
  1. 在每一個volatile寫操做前面插入StoreStore屏障
  2. 在每一個volatile寫操做後插入StoreLoad屏障
  3. 在每一個volatile讀後面插入一個LoadLoad屏障
  4. 在每一個volatile讀後面插入一個LoadStore屏障

這裏要說下內存屏障是是什麼東西:硬件層的內存屏障分爲兩種: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 來看下這個文件的編譯指令:


increase方法的編譯指令咱們能夠看出 ++ 操做經歷了4步:
一、getstatic #10 獲取靜態變量num壓入棧頂 此時volatile保證值是對的。
二、iconst_1 int型常量1入棧
三、iadd 棧頂兩個int值相加,結果放入棧頂。
四、putstatic #10 把棧頂的值負值給指定域。
問題就出在二、3兩步,在作這兩步操做時,volatile變量有可能已經被其它線程修改。

根據volatile的內存語意咱們能夠總結出兩條安全使用volatile的方式:
  • 運算結果不依賴於volatile變量的當前值,或者能保證只有單一線程能修改變量的值
  • 變量不須要與其它的狀態變量共同參與不變性。
相關文章
相關標籤/搜索