一 引言
聽說在Java 5之前volatile關鍵字備受爭議,所以本文也不討論1.5版本之前的volatile。本文主要針對1.5后即JSR-133針對volatile做了強化后的了解。
二 volatile的特性
開門見山,volatile變量自身具有以下特性:
- 可見性(最重要的特性)。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
- 原子性。對任意(包括64位long類型和double類型)單個volatile變量的讀/寫具有原子性。但是類型於a++這種復合操作不具有原子性。
下面通過案例來證明下可見性,先看一個普通變量是否能保證可見性:
3 class Example {
4 private boolean stop = false; 5 public void execute() { 6 int i = 0; 7 System.out.println("thread1 start loop."); 8 while(!getStop()) { 9 i++; 10 } 11 System.out.println("thread1 finish loop,i=" + i); 12 } 13 public boolean getStop() { 14 return stop; // 對普通變量的讀 15 } 16 public void setStop(boolean flag) { 17 this.stop = flag; // 對普通變量的寫 18 } 19 } 20 public class VolatileExample { 21 public static void main(String[] args) throws Exception { 22 final Example example = new Example(); 23 Thread t1 = new Thread(new Runnable() { 24 @Override 25 public void run() { 26 example.execute(); 27 } 28 }); 29 t1.start(); 30 31 Thread.sleep(1000); 32 System.out.println("主線程即將置stop值為true..."); 33 example.setStop(true); 34 System.out.println("主線程已將stop值為:" + example.getStop()); 35 System.out.println("主線程等待線程1執行完..."); 36 37 t1.join(); 38 System.out.println("線程1已執行完畢,整個流程結束..."); 39 } 40 }
上面程序的意思是:讓線程1先執行然后主(main)線程修改標志看是否能讓子線程跳出循環。執行程序后發現程序並沒有執行完,而是在等待線程1執行完畢。這就說明主線程修改stop變量並不對線程1可見,所以普通變量是不保證可見性的。
當你把變量stop用volatile修飾時,主線程修改stop變量會立馬對線程1可見並終止程序,這就證明volatile變量是具有可見性特性的。下面修改后的結果。
原子性特性已經說的很清楚了(對任意(包括64位long類型和double類型)單個volatile變量的讀/寫具有原子性),記着是對單個volatile變量的讀或寫才具有原子性(如果要進行測試的話,將上面案例的volatile變量修改成long/double類型,測試邏輯一樣,只不過將它放在x86的機器上運行。因為在x86的機器上不能保證long類型和double類型的原子性的,具體原因在Java內存模型中的順序一致性一節有說明)。另外任何復合操作都不能保證原子性,如a++,a = a+1, a = b。特別注意a = b這類,它實際上包含2個操作,它先要去讀取b的值,再將b的值寫入工作內存,雖然讀取b的值以及將b的值寫入工作內存這2個操作都是原子性操作,但是合起來就不是原子性操作了。
想要理解透volatile特性有一個很好的方法,就是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。
三 volatile寫-讀建立的happens-before關系
這個詳細在happens-before規則中說明。
四 volatile寫-讀的內存語義
-
當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存。
-
當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
以上面VolatileExample程序為例進行簡單說明,當主線程對stop進行修改后且子線程尚未對stop進行讀時,主線程已經把stop的值刷新到了主內存。其示意圖如下:
當子線程進行讀取時,會把本地內存置為無效直接去主內存中讀取。(這里的主線程和子線程可以了解為兩個普通線程沒有父子關系)其示意圖如下:
五 volatile內存語義的實現
為了實現volatile的內存語義,JMM會分別限制這兩種類型的重排序。下圖是JMM針對編譯器指定的volatile重排序規則表。
- 當第二個操作為volatile寫操作時,不管第一個操作是什么(普通讀寫或者volatile讀寫),都不能進行重排序。這個規則確保volatile寫之前的所有操作都不會被重排序到volatile寫之后;
- 當第一個操作為volatile讀操作時,不管第二個操作是什么,都不能進行重排序。這個規則確保volatile讀之后的所有操作都不會被重排序到volatile讀之前;
- 當第一個操作是volatile寫操作時,第二個操作是volatile讀操作,不能進行重排序。
為了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障(在JMM也有提到過且有說明對應的幾種屏障的作用,請仔細閱讀)來禁止特定類型的處理器重排序。下面是基於保守策略的 JMM 內存屏障插入策略:
-
在每個 volatile 寫操作的前面插入一個 StoreStore 屏障(禁止前面的寫與volatile寫重排序)。
-
在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障(禁止volatile寫與后面可能有的讀和寫重排序)。
-
在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障(禁止volatile讀與后面的讀操作重排序)。
-
在每個 volatile 讀操作的后面插入一個 LoadStore 屏障(禁止volatile讀與后面的寫操作重排序)。
其中重點說下StoreLaod屏障,它是確保可見性的關鍵,因為它會將屏障之前的寫緩沖區中的數據全部刷新到主內存中。上述內存屏障插入策略非常保守,但它可以保證在任意處理平台,任意的程序中都能得到正確的volatile語義。下面是保守策略(為什么說保守呢,因為有些在實際的場景是可省略的)下,volatile 寫操作 插入內存屏障后生成的指令序列示意圖:
其中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作對任意處理器可見(把它刷新到主內存)。另外volatile寫后面有StoreLoad屏障,此屏障的作用是避免volatile寫與后面可能有的讀或寫操作進行重排序。因為編譯器常常無法准確判斷在一個volatile寫的后面是否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)為了保證能正確實現volatile的內存語義,JMM采取了保守策略:在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內存語義的常見模式是:一個寫線程寫volatile變量,多個度線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執行效率的提升。從這里也可看出JMM在實現上的一個特點:首先確保正確性,然后再去追求效率(其實我們工作中編碼也是一樣)。
下面是在保守策略下,volatile讀插入內存屏障后生產的指令序列示意圖:
上述volatile寫和volatile讀的內存屏障插入策略非常保守。在實際執行時,只要不改變volatile寫-讀的內存語義,編譯器可以根據具體情況忽略不必要的屏障。在JMM基礎中就有提到過各個處理器對各個屏障的支持度,其中x86處理器僅會對寫-讀操作做重排序。
六 總結
volatile主要作用是具有可見性和原子性(單個變量),其實現原理就是利用屏障來保障實現。要想徹底掌握就應該多做下相關場景的編碼,經典的場景有:狀態標記量、volatile方式的double check等。
以上如有錯誤之處,歡迎指出,歡迎討論,謝謝!