volatile字段是用于線程間通訊的特殊字段。每次讀volatile字段都會看到其他線程寫入該字段的最新值;實際上,之所以要定義volatile字段是因為在某些情況下由于緩存和重排序所看到的陳舊的變量值是不可接受的。編譯器和運行時禁止在寄存器里分配它們。它們還必須保證,在它們寫好之后,它們被緩沖區刷新到主存中,因此,它們立即能夠對其他線程可見。相同地,在讀取一個volatile字段之前,緩沖區必須失效,因為值是存在于主存中而不是本地處理器緩沖區。在重排序訪問volatile變量的時候還有其他的限制。
在舊的內存模型下,訪問volatile變量不能重排序,但是,它們可能和訪問非volatile變量一起被重排序。這破壞了volatile字段從一個線程到另外一個線程作為一個信號條件的手段。
在新的內存模型下,volatile變量仍然不能彼此重排序。和舊模型不同的是,volatile周圍的普通字段不再隨意的去重排序了。寫入一個volatile字段和釋放監視器有相同的內存影響。事實上,因為新的內存模型在重排序volatile字段訪問上面和其他字段(volatile或者非volatile)訪問上面有了更嚴格的約束。當線程A寫入一個volatile字段f的時候,如果線程B讀取f的話,那么對線程A可見的任何東西都變得對線程B可見了。
如下例子展示了volatile字段應該如何使用:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// users x - guranteed to see 42.
}
}
}
假設一個線程叫做writer
,另外一個線程叫做reader
。對變量v的寫操作會等到變量x寫入到內存之后,然后讀線程就可以看見~v的值。因此,如果reader線程看到了v的值為true,那么,它也保證能夠看到在之前發生的寫入42這個操作。而這在舊的內存模型中卻未必是這樣的。如果v不是volatile變量,那么,編譯器可以在writer線程中重排序寫入操作,那么reader線程中的讀取x變量的操作可能會看到0。
實際上,volatile的語義已經被加強了,已經快達到同步的級別了。為了可見性的原因,每次讀取和寫入一個volatile字段已經像一個半同步操作了。
重點注意
對兩個線程來說,為了正確的設置happens-before關系,訪問相同的volatile變量是很重要的。以下的結論是不正確的:當線程A寫volatile字段f的時候線程A可見的所有東西,在線程B讀取volatile的字段g之后,變得對線程B可見了。釋放操作和獲取操作必須匹配(也是就是在同一個volatile字段上面完成)
雙重鎖檢查
臭名昭著的雙重鎖檢查(也叫多線程單例模式)是一個騙人的把戲,它用來支持lazy初始化,同時避免過度使用同步。在非常早的JVM中,同步非常慢,開發人員非常希望刪掉它。雙重鎖檢查代碼如下:
// double-checked-locking -don't do this!
private static Something instance = null;
public Something getInstance() {
if (instance == null) {
synchronized(this){
if (instance == null) {
instance = new Something();
}
}
}
}
這看起來好像非常聰明---在公用代碼中避免了同步。這段代碼只有一個問題---它不能正常工作。為什么呢?最明顯的原因是,初始化實例的寫入操作和實例字段的寫入操作能夠被編譯器或者緩沖區重排序,重排序可能導致返回部分構造的一些東西。就是我們讀取到了一個沒有初始化的對象。這段代碼還有很多其他的錯誤,以及為什么對這段代碼的算法修正是錯誤的。在舊的Java內存模型下沒有辦法修復它。更多深入的信息可參見:Double-checked locking:Clever but broken 和The "DoubleChecked Locking is broken" declaration
許多人認為使用volatile關鍵字能夠消除雙重檢查模式的問題。在1.5的JVM之前,volatile并不能保證這段代碼正常工作(因環境而定)。在新的內存模型下,實例字段使用volatile可以解決雙重模式檢查的問題,因為在構造線程來初始化一些東西和讀取線程返回它的值之間有happens-before關系。
但是,對于喜歡使用雙重鎖檢查的人來說(我們真的希望沒有人這么做),仍然不是好消息。雙重鎖檢查的重點是為了避免過度使用同步導致性能問題。從java1.0開始,不僅同步會有昂貴的性能開銷,而且在新的內存模型下,使用volatile的性能開銷也有所上升,幾乎達到了和同步一樣的性能開銷。因此,使用雙重鎖檢查來實現單例模式仍然不是一個好的選擇。(修訂---volatile在大多數平臺下性能開銷還是比較低的)。
使用IODH(Initialization Demand Holder)來實現多線程模式下的單例會更易讀:
private static class LazySomethingHolder {
public static Something something = new Something();
}
public static Something getInstance(){
return LazySomethingHolder.something;
}
這段代碼是正確的,因為初始化是由static字段來保證的。如果一個字段設置在static初始化中,對其他訪問這個類的線程來說是能正確的保證它的可見性的。