1. 程式人生 > >Java volatile關鍵字例項

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的情況)。