天天看點

Java并發程式設計之volatile的了解

 Java并發程式設計之volatile關鍵字的了解 

        Java中每個線程都有自己的工作記憶體,類比于處理器的緩存,線程的工作記憶體中儲存了被該線程使用到的變量的主記憶體的拷貝。線程讀寫變量都是直接在自己的工作記憶體中進行的,而何時重新整理資料(指将修改的結果更新到主存或者把主存的變量讀取覆寫掉工作記憶體中的值)是不确定的。

       volatile關鍵字是修飾字段的關鍵字,貌似是JDK1.5之後才有的,在多線程程式設計中,很大的幾率會用到這個關鍵字,volatile修飾變量後該變量有這麼一種效果:線程每一次讀該變量都是直接從主存(JVM的主存)中讀,而不是從線程的工作記憶體中;每一次寫該變量都會同時寫到主存中,而不僅僅是線程的工作記憶體中。是以一開頭說的"何時重新整理資料是不确定的"隻适用于非volatile變量。

JVM對volatile變量有兩個保證:

  1. 可見性。這個上面也大概解釋了,就是某個線程改變了值,另一個線程立馬就能讀到改變後的值,容易了解。
  2. Happens-Before。有兩點要說明:
  • 線程在寫volatile變量時,若對一個普通變量的寫在對該volatile變量的寫之前,那麼對該普通變量的寫也将會被寫到主存,而不僅僅是工作記憶體;線程在讀volatile變量時,若對一個普通變量的讀在對該volatile變量的讀之後,那麼對該普通變量的讀将會先和主存同步,再讀取,而不是直接從工作記憶體中讀。例如  下載下傳

Java代碼 

  1. Thread A:  
  2. object.nonVolatileVar = 1;  // stepA1  
  3. object.volatileVar = object.volatileVar + 1; // stepA2  
  4. Thread B:  
  5. int volatileVar = object.volatileVar; // stepB1  
  6. int nonVolatile = object.nonVolatileVar; // stepB2  

 線程A執行到stepA2,當要把volatileVar的新值寫到主存時,nonVolatileVar的新值也會被刷到主存中;線程B執行到stepB1時,當要從主存中讀object.volatileVar時,object.nonVolatileVar也會被一起讀進工作記憶體,是以當線程 B執行到StepB2時,是可以拿到nonVolatileVar 的最新值的。這種特性其實蠻有用的:當一個線程有多個volatile變量時,可以根據這個特性減少volatile變量(通過變量的讀、寫順序),可以達到和多個volatile變量同樣的效果。

  • 對volatile變量的讀寫指令是不會被JVM重排序的。讀/寫之前或之後的其他指令可以重排序,但對volatile變量的讀/寫指令和其它指令的相對順序是不會改變的。例如  下載下傳
  1. object.nonVolatile1 = 123;  //instruction1  
  2. object.nonVolatile2 = 456;  //instruction2  
  3. object.nonVolatile3 = 789; // //instruction3  
  4. object.volatile     = true; //a volatile variable, //instruction4  
  5. int value1 = sharedObject.nonVolatile4; //instruction5  
  6. int value2 = sharedObject.nonVolatile5;  //instruction6  
  7. int value3 = sharedObject.nonVolatile6;   //instruction7  

 由于JVM發現instruction1、instruction2、instruction3沒有前後作用關系,是以jvm有可能會重排序這三條指令,instruction456也是如此,但是中間有個volatile變量的讀。是以instruction123是不會被重排序到instruction4後面去的,同樣instruction456也不會重排序到instruction4前面去的,他們的相對順序不會變。

一個很常見的用volatile的例子就是單例模式(某種線程安全的寫法):  下載下傳

  1. public class Singleton {  
  2.     private volatile static Singleton instance;  
  3.     public static Singleton getInstance() {  
  4.         if(instance == null) { //step1  
  5.             synchronized (Singleton.class) { //step2  
  6.                 if(instance == null) { //step3  
  7.                     instance = new Singleton(); // step4  
  8.                 }  
  9.             }  
  10.         }  
  11.         return instance;  
  12.     }  
  13.     private Singleton(){}  
  14. }  

  這裡的isntance如果不用volatile修飾,那麼這個單例就是非多線程安全的,知道synchronized有可見性保證的人可能會問:為什麼這裡用了synchronized還需要用volatile修飾?确實,這裡兩者都保證了可見性,但是這裡用volatile不是因為可見性的原因,而是因為指令重排序的原因:首先要知道一點的就是new一個對象時有三步(僞碼):  下載下傳

  1. memory = allocate();   //1:配置設定對象的記憶體空間  
  2. ctorInstance(memory);  //2:初始化對象  
  3. instance = memory;     //3:設定instance指向剛配置設定的記憶體位址  

 而這三條指令肯定都是同一個線程執行,根據intra-thread semantic(intra-thread semantics保證重排序不會改變單線程内的程式執行結果),這三條指令是可以重排序成下面這樣的:

 那麼上面的單例就有問題了。假設不幸上述重排序發生了,那麼初始化對象的線程正好設定了instance = memory(即instance已經不為null了)但是instance還沒被初始化時,另一個線程跑到step1,發現instance不為null,然後直接把instance拿去用了,後面自然就會出現各種問題,因為對象根本還沒被初始化。用了volatile修飾後,上面所說的重排序就被禁止了。 

java.util.concurrent包下用到volatile的地方數不勝數,比如java.util.concurrent.FutureTask<T>中就有使用到volatile的happens-before原則:: 下載下傳

 可以看到有兩個變量state, callable都需要保證其可見性, 但是這裡隻用volatile修飾其中一個,而通過寫的順序來保證不被volatile修飾的那個變量的可見性。

繼續閱讀