據說在Java 5以前volatile關鍵字備受爭議,因此本文也不討論1.5版本以前的volatile。本文主要針對1.5後即JSR-133針對volatile作了強化後的瞭解。html
開門見山,volatile變量自身具備如下特性:app
下面經過案例來證實下可見性,先看一個普通變量是否能保證可見性:ide
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可見,因此普通變量是不保證可見性的。oop
當你把變量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個操做都是原子性操做,可是合起來就不是原子性操做了。this
想要理解透volatile特性有一個很好的方法,就是把對volatile變量的單個讀/寫,當作是使用同一個鎖對這些單個讀/寫操做作了同步。編碼
這個詳細在happens-before規則中說明。spa
當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存。線程
當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。code
以上面VolatileExample程序爲例進行簡單說明,當主線程對stop進行修改後且子線程還沒有對stop進行讀時,主線程已經把stop的值刷新到了主內存。其示意圖以下:
當子線程進行讀取時,會把本地內存置爲無效直接去主內存中讀取。(這裏的主線程和子線程能夠了解爲兩個普通線程沒有父子關係)其示意圖以下:
爲了實現volatile的內存語義,JMM會分別限制這兩種類型的重排序。下圖是JMM針對編譯器指定的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等。
以上若有錯誤之處,歡迎指出,歡迎討論,謝謝!