java并發(fā)編程(十六)帶你了解volatile原理

還記得上一篇文章當中提到的內(nèi)存屏障(Memory Fence)嗎?其實Volatile的實現(xiàn)原理就是通過內(nèi)存屏障來實現(xiàn)的。

  • 對于volatile修飾的變量:

    • 在該變量的寫指令后,會加入寫屏障
    • 在該變量的讀指令前,會加入讀屏障

上面先放個結(jié)論,后面我們逐步的看它是什么意思。

我們看下有如下的代碼,主要是為了理解寫屏障和讀屏障是如何添加,且填在的位置在何處:

public class VolatileTest {

    /**
     * 定義一個volatile修飾的共享變量
     */
    volatile static boolean flag = false;

    /**
     * 定義全局變量num
     */
    static int num = 0;

    public static void test1() {
        num = 2;
        // 此處修改數(shù)據(jù)ready,會增加一個寫屏障,從而num、ready在修改數(shù)據(jù)后,都會添加到主存當中
        flag = true;
    }

    public static void test2() {
        // 此處讀取數(shù)據(jù)ready,會增加一個讀屏障,保證后面的ready和num都會從主存當中獲取數(shù)據(jù)
        if (flag) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {

        new Thread(() -> {
            test1();
        }, "t1").start();

        new Thread(() -> {
            test2();
        }, "t2").start();
    }
}

如上所示,有volatile修飾的變量flag,假設(shè)上述代碼t1先執(zhí)行,t2后執(zhí)行,會有如下過程:

  • t1執(zhí)行test1方法,此時將num賦值稱為2,num此時可能沒有推送到主存當中。之后又執(zhí)行了對flag賦值的操作,因為flag是volatile修飾的,所以一定會將flag更新到主存,同時將num也會更新到主存。

  • t2執(zhí)行test2方法時,首先會讀取flag的值,由于flag是有volatile修飾,此時會從主存拉取flag的值,同時num也會從主存獲取。

一、可見性如何保證?

前文說到,寫屏障對于共享變量的所有修改,在寫屏障前的所有共享變量,都需要同步到主內(nèi)存當中。

讀屏障對于共享變量的所有修改,在讀屏障后的所有共享變量,都需要同從主存當中獲取。

在文章開始的例子當中已經(jīng)闡述了流程:

  • 在修改flag的值時,所依靠的是寫屏障,會在flag被修改后的位置添加一個寫屏障,在寫屏障之前的的num、和flag修改后的值都會同步到主存當中。

  • 在讀取flag的值時,所依靠的是讀屏障,在flag讀取之前增加一份讀屏障,在讀屏障后讀取的flag和num都會從主存當中獲取。

二、有序性如何保證?

  • 寫屏障保證在發(fā)生指令重排序時,不會將寫屏障之前的代碼放在寫屏障之后。

  • 讀屏障會確保指令重排序時,不會將讀屏障后的代碼放在讀屏障之前。

假設(shè)在volatile關(guān)鍵字之前有多個變量被修改的語句,那么volatile是不能保證其執(zhí)行的順序,能保證的僅僅是在寫屏障前的所有代碼都執(zhí)行完畢,并且寫屏障前的修改對于讀屏障后代碼一定是可見的。

假如讀取在寫屏障之前,那么則不能保證了。

另外需要注意的是,有序性只保證在當前線程內(nèi)的代碼不被重排序。

三、happens-before原則

happens-before 規(guī)定了對共享變量的寫操作對其它線程的讀操作可見,可以說它是可見性與有序性的一套規(guī)則總結(jié)。

JMM(java memory model,java內(nèi)存模型)在以下的情況可以保證,線程對共享變量的寫,對于其他線程是讀可見的,最常見的有以下兩種:

  • 使用synchronized關(guān)鍵字

    前面的文章提到過,當使用重量級鎖時,對于共享變量的修改時要同步到主存的。

  • 使用volatile修飾的共享變量

還有以下場景(更多的不在下面舉例了):

  • 當線程修改共享變量的值,其結(jié)束后,其他線程對于修改后的值是可見的。

  • 線程start()之前,對于變量修改后的值,對其是可見的。

  • 線程t1修改變量的值,隨后對正在讀取該變量的t2進行打斷,此時t1打斷線程t2,則t2對于修改后的變量讀可見。

四、Double-Checked Locking

相信同學(xué)們都學(xué)習(xí)過單例模式,應(yīng)該都知道其有很多種實現(xiàn)方式,其中有一種就是double-checked locking(雙重檢查鎖)的方式,如下所示:

public class Singleton {

    /**
     * volatile 解決指令重排序?qū)е碌膯栴}
     */
    private static volatile Singleton instance;

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

    private Singleton() {
    }
}

通過我們的嘗試知道DCL一定要加上volatile關(guān)鍵字去修飾實例變量instance,那么是為什么呢?

我們先假設(shè)沒有加volatile關(guān)鍵字的情況,這種情況下砸多線程情況下是會存在問題的。

如下所示,是在沒有添加volatile關(guān)鍵字時的字節(jié)碼文件:

public class com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton {
  public static com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton getInstance();
    Code:
       0: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
       3: ifnonnull     37
       6: ldc           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      14: ifnonnull     27
      17: new           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
      20: dup
      21: invokespecial #3                  // Method "<init>":()V
      24: putstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      40: areturn
    Exception table:
       from    to  target type
          11    29    32   any
          32    35    32   any
}

我們需要了解的是,jvm創(chuàng)建一個完整的對象實例需要兩個步驟:

  • 實例化一個對象,即new 出來的對象,此時是一個默認的空對象,其屬性等并沒有賦值,只是創(chuàng)建了引用,我們可以認為此時是一個半初始化對象。

  • 初始化步驟,此時需要去調(diào)用對象的構(gòu)造方法,完成屬性的賦值等操作,只有經(jīng)過此步驟才是一個完成的對象。

對應(yīng)到上面的字節(jié)碼文件,分別是以下的代碼:

  • 17:創(chuàng)建一個引用,將引用入棧
  • 20:復(fù)制地址引用,用于后面使用
  • 21:通過前面復(fù)制的地址引用,調(diào)用對象的構(gòu)造方法
  • 24:將引用賦值到靜態(tài)變量instance上

相信同學(xué)們應(yīng)該能夠?qū)?yīng)的上的。

在jvm中呢,如果完全按照上面的步驟執(zhí)行則不會有問題,但是jvm會優(yōu)化為先執(zhí)行24步驟,再執(zhí)行21步驟,那么結(jié)果可想而知,此時靜態(tài)變量是一個半初始化的對象。

當另外的線程來執(zhí)行g(shù)etInstance方法時,獲取靜態(tài)實例對象instance,即字節(jié)碼文件的第0行,此行代碼是在鎖synchronized(管程monitorenter)之外,誰來都可以執(zhí)行,那么獲取到了就是半初始對象,不是null,那么一定是有問題的。

通過我們前面的學(xué)習(xí),就可以用volatile來解決DCL的這個問題:

這個volatile關(guān)鍵字在字節(jié)碼是體現(xiàn)不出來的,但是手動標記一下它的位置,只保留主要位置:

       0: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
       --------------------- 此處加入讀屏障 --------------------
       3: ifnonnull     37
       6: ldc           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      14: ifnonnull     27
      17: new           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
      20: dup
      21: invokespecial #3                  // Method "<init>":()V
      24: putstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      --------------------- 此處加入寫屏障 --------------------
      27: aload_0
      28: monitorexit

但是根據(jù)我們前面學(xué)習(xí)的,寫屏障似乎并不能保證21和24的順序不變啊,因為都是在寫屏障之前,它只能保證寫屏障之前的代碼不會被放到寫屏障后。那么它是如何解決的呢?

其實在更加底層volatile轉(zhuǎn)成匯編語言,是在該代碼上增加了lock前綴,此時會將其之前的代碼鎖住,直到執(zhí)行到這個lock,此時前面的代碼都一定執(zhí)行完了。

從根本說volatile的實現(xiàn)是是一條CPU原語 lock addl。

太過底層就不多贅述了,畢竟我也沒學(xué)到位呢?。。?!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容