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

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

  • 對(duì)于volatile修飾的變量:

    • 在該變量的寫指令后,會(huì)加入寫屏障
    • 在該變量的讀指令前膊毁,會(huì)加入讀屏障

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

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

public class VolatileTest {

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

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

    public static void test1() {
        num = 2;
        // 此處修改數(shù)據(jù)ready母赵,會(huì)增加一個(gè)寫屏障,從而num具滴、ready在修改數(shù)據(jù)后凹嘲,都會(huì)添加到主存當(dāng)中
        flag = true;
    }

    public static void test2() {
        // 此處讀取數(shù)據(jù)ready,會(huì)增加一個(gè)讀屏障抵蚊,保證后面的ready和num都會(huì)從主存當(dāng)中獲取數(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í)行谷醉,會(huì)有如下過程:

  • t1執(zhí)行test1方法,此時(shí)將num賦值稱為2冈闭,num此時(shí)可能沒有推送到主存當(dāng)中俱尼。之后又執(zhí)行了對(duì)flag賦值的操作,因?yàn)閒lag是volatile修飾的萎攒,所以一定會(huì)將flag更新到主存遇八,同時(shí)將num也會(huì)更新到主存。

  • t2執(zhí)行test2方法時(shí)耍休,首先會(huì)讀取flag的值刃永,由于flag是有volatile修飾,此時(shí)會(huì)從主存拉取flag的值羊精,同時(shí)num也會(huì)從主存獲取斯够。

一、可見性如何保證喧锦?

前文說到读规,寫屏障對(duì)于共享變量的所有修改,在寫屏障前的所有共享變量燃少,都需要同步到主內(nèi)存當(dāng)中束亏。

讀屏障對(duì)于共享變量的所有修改,在讀屏障后的所有共享變量阵具,都需要同從主存當(dāng)中獲取碍遍。

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

  • 在修改flag的值時(shí)定铜,所依靠的是寫屏障,會(huì)在flag被修改后的位置添加一個(gè)寫屏障雀久,在寫屏障之前的的num宿稀、和flag修改后的值都會(huì)同步到主存當(dāng)中趁舀。

  • 在讀取flag的值時(shí)赖捌,所依靠的是讀屏障,在flag讀取之前增加一份讀屏障矮烹,在讀屏障后讀取的flag和num都會(huì)從主存當(dāng)中獲取越庇。

二、有序性如何保證奉狈?

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

  • 讀屏障會(huì)確保指令重排序時(shí)仁期,不會(huì)將讀屏障后的代碼放在讀屏障之前桑驱。

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

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

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

三橡伞、happens-before原則

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

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

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

    前面的文章提到過,當(dāng)使用重量級(jí)鎖時(shí)翘县,對(duì)于共享變量的修改時(shí)要同步到主存的最域。

  • 使用volatile修飾的共享變量

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

  • 當(dāng)線程修改共享變量的值,其結(jié)束后锈麸,其他線程對(duì)于修改后的值是可見的镀脂。

  • 線程start()之前,對(duì)于變量修改后的值忘伞,對(duì)其是可見的薄翅。

  • 線程t1修改變量的值沙兰,隨后對(duì)正在讀取該變量的t2進(jìn)行打斷,此時(shí)t1打斷線程t2翘魄,則t2對(duì)于修改后的變量讀可見鼎天。

四、Double-Checked Locking

相信同學(xué)們都學(xué)習(xí)過單例模式暑竟,應(yīng)該都知道其有很多種實(shí)現(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)鍵字去修飾實(shí)例變量instance但荤,那么是為什么呢罗岖?

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

如下所示桑包,是在沒有添加volatile關(guān)鍵字時(shí)的字節(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)建一個(gè)完整的對(duì)象實(shí)例需要兩個(gè)步驟:

  • 實(shí)例化一個(gè)對(duì)象纺非,即new 出來的對(duì)象哑了,此時(shí)是一個(gè)默認(rèn)的空對(duì)象,其屬性等并沒有賦值烧颖,只是創(chuàng)建了引用弱左,我們可以認(rèn)為此時(shí)是一個(gè)半初始化對(duì)象。

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

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

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

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

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

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

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

這個(gè)volatile關(guān)鍵字在字節(jié)碼是體現(xiàn)不出來的耸携,但是手動(dòng)標(biāo)記一下它的位置辕翰,只保留主要位置:

       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的順序不變啊喜命,因?yàn)槎际窃趯懫琳现埃荒鼙WC寫屏障之前的代碼不會(huì)被放到寫屏障后河劝。那么它是如何解決的呢壁榕?

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

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

太過底層就不多贅述了,畢竟我也沒學(xué)到位呢3佟L痢!挺尿!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奏黑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子编矾,更是在濱河造成了極大的恐慌熟史,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窄俏,死亡現(xiàn)場(chǎng)離奇詭異蹂匹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)裆操,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門怒详,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炉媒,“玉大人,你說我怎么就攤上這事昆烁〉踔瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵静尼,是天一觀的道長(zhǎng)白粉。 經(jīng)常有香客問我,道長(zhǎng)鼠渺,這世上最難降的妖魔是什么鸭巴? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮拦盹,結(jié)果婚禮上鹃祖,老公的妹妹穿的比我還像新娘。我一直安慰自己普舆,他們只是感情好恬口,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沼侣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛾洛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天钞螟,我揣著相機(jī)與錄音扶供,去河邊找鬼。 笑死太援,一個(gè)胖子當(dāng)著我的面吹牛扳碍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碱蒙,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼赛惩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了喷兼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤吠各,失蹤者是張志新(化名)和其女友劉穎贾漏,沒想到半個(gè)月后藕筋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡困食,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了符匾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甸各,死狀恐怖趣倾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情黔漂,我是刑警寧澤炬守,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站曹洽,受9級(jí)特大地震影響衣洁,放射性物質(zhì)發(fā)生泄漏抖仅。R本人自食惡果不足惜撤卢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一智听、第九天 我趴在偏房一處隱蔽的房頂上張望到推。 院中可真熱鬧莉测,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)哥捕。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間记舆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留府瞄,地道東北人碘箍。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓丰榴,卻偏偏與公主長(zhǎng)得像四濒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盗蟆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容