Java volatile關鍵字例項
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
2)禁止進行指令重排序。
volatile關鍵字能保證可見性和有序性,但是不保證原子性。因此並不能保證執行緒安全。
看一個相關的例子:雙重校驗鎖實現的單例模式:
public class DoubleCheckSymbol {
private static volatile DoubleCheckSymbol d;
private DoubleCheckSymbol () {}
public static DoubleCheckSymbol getSymbol() {
if (d == null) {
synchronized(DoubleCheckSymbol.class) {
if (d == null) {
d = new DoubleCheckSymbol();
}
}
}
return d;
}
}
這個單例模式中為什麼要加volatile關鍵字呢?
如果不加volatile的話,會有如下隱患:
d = new DoubleCheckSymbol()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1, 給d分配記憶體。
2, 呼叫 DoubleCheckSymbol的建構函式來初始化成員變數。
3, 將d物件指向分配的記憶體空間(執行完這步 d就為非 null 了)。
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,但是由於它並未初始化,所以就可能發生錯誤。
此例中,synchronized關鍵字已經解決了原子性問題。同時也解決了可見性問題,因為synchronized能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。
然而有序性問題並沒有解決,所以,這就是在這裡使用volatile的目的,即為了防止指令順序的重排序。
另外,如果在JDK1.5之前這樣用volatile的話,可能會出現異常結果。此前的JDK中即使將變數宣告為volatile也不能完全避免重排序導致的問題(主要是volatile變數前後的程式碼仍然存在重排序問題)。
再看一個例子:
package com.lwc.test;
import java.util.concurrent.CountDownLatch;
public class Counter {
private static volatile int value;
private static CountDownLatch countDownLatch = new CountDownLatch(10000);
public static void main(String[] args) throws Exception{
for (int i=0;i<10000;i++){
new Thread(){
@Override
public void run() {
increment();
countDownLatch.countDown();
}
} .start();
}
countDownLatch.await();
System.out.println(getValue());
}
public static int increment(){
return value ++;
}
public static int getValue(){
return value;
}
}
輸出:可能是10000,也可能是小於10000的數,同樣是因為volatile不能保證原子性(value ++並不是原子性操作,所以會出現兩個執行緒同時取得了相同的value值,然後分別+1,然後各自寫入記憶體,結果value只增加了1的情況)。