java關鍵字volatile的作用

自從jdk1.5以後,volatile可謂發生了翻天覆地的變化,從一個一直被吐槽的關鍵詞,變成一個輕量級的線程通信代名詞。

接下來我們將從以下幾個方面來分析以下volatile

  • 重排序與as if serial的關係

  • volatile的特點

  • volatile的內存語義

  • volatile的使用場景

重排序與as if serial的關係

重排序值得是編譯器與處理器爲了優化程序的性能,而對指令序列進行重新排序的。

但是並不是什麼情況下都可以重排序的,

  • 數據依賴

    a = 1;            // 1
    b = 2;            // 2
    

    在這種情況,1、2不存在數據依賴,是可以重排序的。

    a = 1;            // 1
    b = a;            // 2    
    

    在這種情況,1、2存在數據依賴,是禁止重排序的。

  • as if serial

    簡單的理解就是。不管怎麼重排序,在單線程情況下程序的執行結果是一致。

根據 as if serial原則,它強調了單線程。那麼多線程發生重排序又是怎麼樣的呢?

請看下面代碼

public class VolatileExample1 {

    /**
     * 共享變量 name
     */
    private static String name = "init";


    /**
     * 共享變量 flag
     */
    private  static boolean flag = false;


    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            name = "yukong";            // 1
            flag = true;                // 2
        });
        Thread threadB = new Thread(() -> {
            if (flag) {                 // 3
                System.out.println("flag = " + flag + " name = " +name);  // 4
            };
        });
    }
}

上面代碼中,name輸出一定是yukong嗎,答案是不一定,根據happen-before原則與as if serial

原則,由於 1、2不存在依賴關係,可以重排序,操作3、操作4也不存在數據依賴,也可以重排序。

那麼就有可能發生下面的情況

 

 

1535967197003.png

上圖中,操作1與操作2發生了重排序,程序運行的時候,線程A先將flag更改成true,然後線程B讀取flag變量並且判斷,由於此時flag已經是true,線程B將繼續讀取name的值,由於此時線程name的值還沒有被線程A寫入,那麼線程此時輸出的name就是初始值,因爲在多線程的情況下,重排序存在線程安全問題。

volatile的特點

volatile變量具有以下的特點。

  • 可見性。對於一個volatile變量的讀,總是能看到任意線程對這個變量的最後的修改。
  • 有序性。對於存在指令重排序的情況,volatile會禁止部分指令重排序。

這裏我先介紹一下volatile關鍵詞的特點,接下來我們將會從它的內存語義來解釋,爲什麼它會具有以上的特點,以及它使用的場景。

volatile的內存語義

  • 當寫一個volatile變量時,JMM會立即將本地變量中對應的共享變量值刷新到主內存中。
  • 當讀一個volatile變量時,JMM會將線程本地變量存儲的值,置爲無效值,線程接下來將從主內存中讀取共享變量。

如果一個場景存在對volatile變量的讀寫場景,在讀線程B讀一個volatile變量後,,寫線程A在寫這個volatile變量前所有的所見的共享變量的值都將會立即變得對讀線程B可見。

那麼這種內存語義是怎麼實現的呢?

其實編譯器生產字節碼的時候,會在指令序列中插入內存屏障來禁止指令排序。下面就是JMM內存屏障插入的策略。

  • 在每一個volatile寫操作前插入一個StoreStore屏障
  • 在每一個volatile寫操作後插入一個StoreLoad屏障
  • 在每一個volatile寫操作後插入一個LoadLoad屏障
  • 在每一個volatile讀操作後插入一個LoadStore屏障

那麼這些策略中,插入這些屏障有什麼作用呢?我們逐條逐條分析一下。

  • 在每一個volatile寫操作前插入一個StoreStore屏障,這條策略保證了volatile寫變量與之前的普通變量寫不會重排序,即是隻有當volatile變量之前的普通變量寫完,volatile變量纔會寫。 這樣就保證volatile變量寫不會跟它之前的普通變量寫重排序
  • 在每一個volatile寫操作後插入一個StoreLoad屏障,這條策略保證了volatile寫變量與之後的volatile寫/讀不會重排序,即是隻有當volatile變量寫完之後,你後面的volatile讀寫才能操作。 這樣就保證volatile變量寫不會跟它之後的普通變量讀重排序
  • 在每一個volatile讀操作後插入一個LoadLoad屏障,這條策略保證了volatile讀變量與之後的普通讀不會重排序,即只有當前volatile變量讀完,之後的普通讀才能讀。 這樣就保證volatile變量讀不會跟它之後的普通變量讀重排序
  • 在每一個volatile讀操作後插入一個LoadStore屏障,這條策略保證了volatile讀變量與之後的普通寫不會重排序,即只有當前volatile變量讀完,之後的普通寫才能寫。樣就保證volatile變量讀不會跟它之後的普通變量寫重排序

根據這些策略,volatile變量禁止了部分的重排序,這樣也是爲什麼我們會說volatile具有一定的有序的原因。

根據以上分析的volatile的內存語義,大家也就知道了爲什麼前面我們提到的happen-before原則會有一條

  • volatile的寫happen-before與volaile的讀。

那麼根據volatile的內存語義,我們只需要更改之前的部分代碼,只能讓它正確的執行。

即把flag定義成一個volatile變量即可。

public class VolatileExample1 {

    /**
     * 共享變量 name
     */
    private static String name = "init";


    /**
     * 共享變量 flag
     */
    private  volatile static boolean flag = false;


    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            name = "yukong";            // 1
            flag = true;                // 2
        });
        Thread threadB = new Thread(() -> {
            if (flag) {                 // 3
                System.out.println("flag = " + flag + " name = " +name);  // 4
            };
        });
    }
}

我們來分析一下

  • 由於 flag是volatile變量 那麼在volatile寫之前插入一個storestore內存屏障,所以1,2不會發生重排序,即1happen before 2

  • 由於 flag是volatile變量 那麼在volatile讀之後插入一個loadload內存屏障,所以3,4不會發生重排序,即3happen before 4

  • 根據happen-before原則,volatile寫happen before volatile讀,即是 2happen before 3。

  • 根據happen-before的傳遞性,所以1 happen before4。

1535969126569.png

volatile的使用場景

  • 標記變量,也就是上面的flag使用
  • double check 單例模式中

下面我們看看double check的使用

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

至於爲何需要這麼寫請參考:

《Java 中的雙重檢查(Double-Check)》http://www.iteye.com/topic/652440

喜歡的小夥伴可以點下關注,後續還會有相關服務器搭建系列文章繼續更新!!!

阿里雲服務器優惠連接,助你早日成就架構師之夢

https://www.aliyun.com/minisite/goods?userCode=0xl4npky

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章