Java併發編程之volatile關鍵字的理解
Java中每個線程都有自己的工作內存,類比於處理器的緩存,線程的工作內存中保存了被該線程使用到的變量的主內存的拷貝。線程讀寫變量都是直接在自己的工作內存中進行的,而何時刷新數據(指將修改的結果更新到主存或者把主存的變量讀取覆蓋掉工作內存中的值)是不確定的。
volatile關鍵字是修飾字段的關鍵字,貌似是JDK1.5之後纔有的,在多線程編程中,很大的機率會用到這個關鍵字,volatile修飾變量後該變量有這麼一種效果:線程每一次讀該變量都是直接從主存(JVM的主存)中讀,而不是從線程的工作內存中;每一次寫該變量都會同時寫到主存中,而不僅僅是線程的工作內存中。因此一開頭說的"何時刷新數據是不確定的"只適用於非volatile變量。
JVM對volatile變量有兩個保證:
可見性。這個上面也大概解釋了,就是某個線程改變了值,另一個線程立馬就能讀到改變後的值,容易理解。
Happens-Before。有兩點要說明:
線程在寫volatile變量時,若對一個普通變量的寫在對該volatile變量的寫之前,那麼對該普通變量的寫也將會被寫到主存,而不僅僅是工作內存;線程在讀volatile變量時,若對一個普通變量的讀在對該volatile變量的讀之後,那麼對該普通變量的讀將會先和主存同步,再讀取,而不是直接從工作內存中讀。例如 下載
Java代碼
Thread A:
object.nonVolatileVar = 1; // stepA1
object.volatileVar = object.volatileVar + 1; // stepA2
Thread B:
int volatileVar = object.volatileVar; // stepB1
int nonVolatile = object.nonVolatileVar; // stepB2
線程A執行到stepA2,當要把volatileVar的新值寫到主存時,nonVolatileVar的新值也會被刷到主存中;線程B執行到stepB1時,當要從主存中讀object.volatileVar時,object.nonVolatileVar也會被一起讀進工作內存,因此當線程 B執行到StepB2時,是可以拿到nonVolatileVar 的最新值的。這種特性其實蠻有用的:當一個線程有多個volatile變量時,可以根據這個特性減少volatile變量(通過變量的讀、寫順序),可以達到和多個volatile變量同樣的效果。
對volatile變量的讀寫指令是不會被JVM重排序的。讀/寫之前或之後的其他指令可以重排序,但對volatile變量的讀/寫指令和其它指令的相對順序是不會改變的。例如 下載
Java代碼
object.nonVolatile1 = 123; //instruction1
object.nonVolatile2 = 456; //instruction2
object.nonVolatile3 = 789; // //instruction3
object.volatile = true; //a volatile variable, //instruction4
int value1 = sharedObject.nonVolatile4; //instruction5
int value2 = sharedObject.nonVolatile5; //instruction6
int value3 = sharedObject.nonVolatile6; //instruction7
由於JVM發現instruction1、instruction2、instruction3沒有前後作用關係,因此jvm有可能會重排序這三條指令,instruction456也是如此,但是中間有個volatile變量的讀。因此instruction123是不會被重排序到instruction4後面去的,同樣instruction456也不會重排序到instruction4前面去的,他們的相對順序不會變。
一個很常見的用volatile的例子就是單例模式(某種線程安全的寫法): 下載
Java代碼
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if(instance == null) { //step1
synchronized (Singleton.class) { //step2
if(instance == null) { //step3
instance = new Singleton(); // step4
}
}
}
return instance;
}
private Singleton(){}
}
這裏的isntance如果不用volatile修飾,那麼這個單例就是非多線程安全的,知道synchronized有可見性保證的人可能會問:爲什麼這裏用了synchronized還需要用volatile修飾?確實,這裏兩者都保證了可見性,但是這裏用volatile不是因爲可見性的原因,而是因爲指令重排序的原因:首先要知道一點的就是new一個對象時有三步(僞碼): 下載
Java代碼
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
而這三條指令肯定都是同一個線程執行,根據intra-thread semantic(intra-thread semantics保證重排序不會改變單線程內的程序執行結果),這三條指令是可以重排序成下面這樣的:
Java代碼
memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址
ctorInstance(memory); //2:初始化對象
那麼上面的單例就有問題了。假設不幸上述重排序發生了,那麼初始化對象的線程正好設置了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修飾的那個變量的可見性。
書上看到的:“除了volatile外,synchronized和final也能實現可見性。synchronized的可見性:在離開synchronized代碼塊前,必須先把變量同步到主存中。final的可見性:被final修飾的字段在構造器中一旦完成初始化,並且構造器沒有把this引用傳遞出去,那在其他線程中就能看到這個值”。