Java 多線程(6):volatile 關鍵字的使用

volatile 做爲 Java 語言的一個關鍵字,被看做是輕量級的 synchronized(鎖)。雖然 volatile 只具備synchronized 的部分功能,可是通常使用 volatile 會比使用 synchronized 更有效率。在編寫多線程程序的時候,volatile 修飾的變量可以:html

  1. 保證內存 可見性
  2. 防止指令 重排序
  3. 保證對 64 位變量 讀寫的原子性

一. 保證內存可見性

JVM 中,每一個線程都擁有本身棧內存,用來保存當前線程運行過程當中的變量數據;而後多個線程之間共享堆內存(也稱主存)。當線程須要訪問一個變量時,首先將其從堆內存中複製到本身的棧內存做爲副本,而後線程每次對該變量的操做,都將是對棧中的副本進行操做 —— 在某些時刻(好比退出 synchronized 塊或線程結束),線程會將棧中副本的值寫回到主存,此時主存中的變量纔會被替換爲副本的值。這樣天然就帶來一個問題,即若是兩個線程共享一個變量,線程A 改變了變量的值,可是 線程B 可能沒法當即發現。好比下面這個經典的例子:java

public class ConcurrentTest {

    private static boolean running = true;

    public static class AnotherThread extends Thread {

        @Override
        public void run() {
            System.out.println("AnotherThread is running");

            while (running) { }

            System.out.println("AnotherThread is stoped");
        }

    }

    public static void main(String[] args) throws Exception {
        new AnotherThread ().start();

        Thread.sleep(1000);
        running = false;  // 1 秒以後想中止 AnotherThread 
    }
}

上面這段代碼通常狀況下都會死鎖,就是由於在 main 方法(主線程)中對 running 作的修改,並不能立馬對 AnotherThread 可見。多線程

若是將 running 加上修飾符 volatile,那麼即可以獲取實際但願的結果,由於此時主線程中設置 runningfalse 以後,AnotherThread 能夠立馬發現 running 的值發生了改變:
實際但願的結果併發

對於 volatile 修飾的變量,JVM 能夠保證:ide

  1. 每次對該變量的寫操做,都將當即同步到主存;
  2. 每次對該變量的讀操做,都將從主存讀取,而不是線程棧

二. 防止指令重排序

若是一個操做不是原子操做,那麼 JVM 即可能會對該操做涉及的指令進行 重排序。重排序即在不改變程序語義的前提下,經過調整指令的執行順序,儘量達到提升運行效率的目的。spa

對於單例模式,爲了達到延時初始化,而且能夠在多線程環境下使用,咱們能夠直接使用 synchronized 關鍵字:線程

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public synchronized static Singleton getSingleton() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

這樣作的缺陷也很明顯,那就是 instance 初始化完畢以後,之後每次獲取 instance 仍然須要進行加鎖操做,是個很大的效率浪費。code

因而出現了一種經典寫法叫 「雙重檢測鎖」:htm

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

可是這樣的寫法一樣會存在問題,由於 instance = new Singleton() 並不是原子操做,其大概能夠等同於執行:blog

  1. 分配一個 Singleton 對應的內存
  2. 初始化這個 Singleton 對應的內存
  3. instance 指向對應的內存的地址

其中,2 依賴於 1,可是 3 並不依賴於 2 —— 因此,存在 JVM 將這三條語句重排序爲 1->3->2 的可能,即變爲:

a. 分配一個 Singleton 對應的內存
b.instance 指向對應的內存的地址
c. 初始化這個 Singleton 對應的內存

此時若是 線程A 執行完 b,那麼此時的 instance 指向的內存並不爲 null,然而這塊內存卻尚未被初始化。當 線程B 此時判斷第一個 if (instance == null) 時發現 instance 並不爲 null,便會將此時的 instance 返回 —— 但 Singleton 的初始化可能並未完成,此時 線程B 使用 instance 即可能會出現錯誤。

在 JDK 1.5 以後,加強了 volatile 的語義,嚴格限制 JVM (編譯器、處理器)不能對 volatile 修飾的變量涉及的操做指令進行重排序。

因此爲了不對 instance 變量涉及的操做進行重排序,保證 「雙重檢測鎖」 的正確性,咱們能夠將 instance 使用 volatile 修飾:

public class Singleton {

    /* 使用 volatile 修飾 */
    public static volatile Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

三. 保證對 64 位變量讀寫的原子性

JVM 能夠保證對 32位 數據讀寫的原子性,可是對於 longdouble 這樣 64位 的數據的讀寫,會將其分爲 高32位 和 低32位 分兩次讀寫。因此對於longdouble 的讀寫並非原子性的,這樣在併發程序中共享 longdouble 變量就可能會出現問題,因而 JVM 提供了 volatile 關鍵字來解決這個問題:

使用 volatile 修飾的 longdouble 變量,JVM 能夠保證對其讀寫的原子性。

但值得注意的是,此處的 「寫」 僅指對 64位 的變量進行直接賦值。而對於 i++ 這個語句,事實上涉及了 讀取-修改-寫入 三個操做:

  1. 讀取變量到棧中某個位置
  2. 對棧中該位置的值進行自增
  3. 將自增後的值寫回到變量對應的存儲位置

所以哪怕變量 i 使用 volatile 修飾,也並不能使涉及上面三個操做的 i++ 具備原子性。因此多線程條件下使用 volatile 關鍵字的前提是:對變量的寫操做不依賴於變量的當前值,而賦值操做很明顯知足這一前提。


在多線程環境下,正確使用 volatile 關鍵字能夠比直接使用 synchronized 更加高效並且代碼簡潔,可是使用 volatile 關鍵字也更容易出錯。因此,除非十分清楚 volatile 的使用場景,不然仍是應該選擇更加具備保障性的 synchronized

Brian Goetz 大大寫過一篇 「volatile 變量使用指南」,有興趣的讀者能夠參閱:Java 理論與實踐: 正確使用 Volatile 變量

volatile 變量的底層實現原理,有興趣的讀者能夠參閱:

  1. http://www.infoq.com/cn/artic...
  2. http://www.cnblogs.com/paddix...
相關文章
相關標籤/搜索