java volatile關鍵字解惑

轉(zhuǎn)載請注明原創(chuàng)出處,謝謝旁趟!
簡書占小狼
http://www.reibang.com/users/90ab66c248e6/latest_articles

前言

看著上一篇的更新時間,發(fā)現(xiàn)已經(jīng)挺長時間沒有提筆了桦山,只能以忙為自己開脫了本慕,如果太閑都不好意思說自己是程序猿了,正好今天有人問了我一個問題:

當一個共享變量被volatile修飾時唱矛,它會保證修改的值立即被更新到主存“, 這里的”保證“ 是如何做到的井辜?和 JIT的具體編譯后的CPU指令相關吧绎谦?

最一開始碰到volatile,我的內(nèi)心是拒絕的粥脚,因為當時做的項目中沒有用到窃肠,也不清楚可以在什么場景下使用,所以希望這篇文章可以幫助大家理解volatile關鍵字刷允。

volatile特性

內(nèi)存可見性:通俗來說就是冤留,線程A對一個volatile變量的修改碧囊,對于其它線程來說是可見的,即線程每次獲取volatile變量的值都是最新的纤怒。

volatile的使用場景

通過關鍵字sychronize可以防止多個線程進入同一段代碼糯而,在某些特定場景中,volatile相當于一個輕量級的sychronize泊窘,因為不會引起線程的上下文切換熄驼,但是使用volatile必須滿足兩個條件:
1、對變量的寫操作不依賴當前值州既,如多線程下執(zhí)行a++谜洽,是無法通過volatile保證結(jié)果準確性的;
2吴叶、該變量沒有包含在具有其它變量的不變式中,這句話有點拗口序臂,看代碼比較直觀蚌卤。

public class NumberRange {
    private volatile int lower = 0;
     private volatile int upper = 10;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

上述代碼中,上下界初始化分別為0和10奥秆,假設線程A和B在某一時刻同時執(zhí)行了setLower(8)和setUpper(5)逊彭,且都通過了不變式的檢查,設置了一個無效范圍(8, 5)构订,所以在這種場景下侮叮,需要通過sychronize保證方法setLower和setUpper在每一時刻只有一個線程能夠執(zhí)行。

下面是我們在項目中經(jīng)常會用到volatile關鍵字的兩個場景:

1悼瘾、狀態(tài)標記量
在高并發(fā)的場景中囊榜,通過一個boolean類型的變量isopen,控制代碼是否走促銷邏輯亥宿,該如何實現(xiàn)卸勺?

public class ServerHandler {
    private volatile isopen;
    public void run() {
        if (isopen) {
           //促銷邏輯
        } else {
          //正常邏輯
        }
    }
    public void setIsopen(boolean isopen) {
        this.isopen = isopen
    }
}

場景細節(jié)無需過分糾結(jié),這里只是舉個例子說明volatile的使用方法烫扼,用戶的請求線程執(zhí)行run方法曙求,如果需要開啟促銷活動,可以通過后臺設置映企,具體實現(xiàn)可以發(fā)送一個請求悟狱,調(diào)用setIsopen方法并設置isopen為true,由于isopen是volatile修飾的堰氓,所以一經(jīng)修改挤渐,其他線程都可以拿到isopen的最新值,用戶請求就可以執(zhí)行促銷邏輯了豆赏。

2挣菲、double check
單例模式的一種實現(xiàn)方式富稻,但很多人會忽略volatile關鍵字,因為沒有該關鍵字白胀,程序也可以很好的運行椭赋,只不過代碼的穩(wěn)定性總不是100%,說不定在未來的某個時刻或杠,隱藏的bug就出來了哪怔。

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

不過在眾多單例模式的實現(xiàn)中,我比較推薦懶加載的優(yōu)雅寫法Initialization on Demand Holder(IODH)向抢。

public class Singleton {  
    static class SingletonHolder {  
        static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.instance;  
    }  
}  

當然认境,如果不需要懶加載的話,直接初始化的效果更好挟鸠。

如何保證內(nèi)存可見性叉信?

在java虛擬機的內(nèi)存模型中,有主內(nèi)存和工作內(nèi)存的概念艘希,每個線程對應一個工作內(nèi)存硼身,并共享主內(nèi)存的數(shù)據(jù),下面看看操作普通變量和volatile變量有什么不同:

1覆享、對于普通變量:讀操作會優(yōu)先讀取工作內(nèi)存的數(shù)據(jù)佳遂,如果工作內(nèi)存中不存在,則從主內(nèi)存中拷貝一份數(shù)據(jù)到工作內(nèi)存中撒顿;寫操作只會修改工作內(nèi)存的副本數(shù)據(jù)丑罪,這種情況下,其它線程就無法讀取變量的最新值凤壁。

2吩屹、對于volatile變量,讀操作時JMM會把工作內(nèi)存中對應的值設為無效客扎,要求線程從主內(nèi)存中讀取數(shù)據(jù)祟峦;寫操作時JMM會把工作內(nèi)存中對應的數(shù)據(jù)刷新到主內(nèi)存中,這種情況下徙鱼,其它線程就可以讀取變量的最新值宅楞。

volatile變量的內(nèi)存可見性是基于內(nèi)存屏障(Memory Barrier)實現(xiàn)的,什么是內(nèi)存屏障袱吆?內(nèi)存屏障厌衙,又稱內(nèi)存柵欄,是一個CPU指令绞绒。在程序運行時婶希,為了提高執(zhí)行性能,編譯器和處理器會對指令進行重排序蓬衡,JMM為了保證在不同的編譯器和CPU上有相同的結(jié)果喻杈,通過插入特定類型的內(nèi)存屏障來禁止特定類型的編譯器重排序和處理器重排序彤枢,插入一條內(nèi)存屏障會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。

這段文字顯得有點蒼白無力筒饰,不如來段簡明的代碼:

class Singleton {
    private volatile static Singleton instance;
    private int a;
    private int b;
    private int b;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    a = 1;  // 1
                     b = 2;  // 2
                    instance = new Singleton();  // 3
                    c = a + b;  // 4
                }
            }
        }
        return instance;
    } 
}

1缴啡、如果變量instance沒有volatile修飾,語句1瓷们、2业栅、3可以隨意的進行重排序執(zhí)行,即指令執(zhí)行過程可能是3214或1324谬晕。
2碘裕、如果是volatile修飾的變量instance,會在語句3的前后各插入一個內(nèi)存屏障攒钳。

通過觀察volatile變量和普通變量所生成的匯編代碼可以發(fā)現(xiàn)帮孔,操作volatile變量會多出一個lock前綴指令:

Java代碼:
instance = new Singleton();

匯編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);

這個lock前綴指令相當于上述的內(nèi)存屏障,提供了以下保證:
1不撑、將當前CPU緩存行的數(shù)據(jù)寫回到主內(nèi)存你弦;
2、這個寫回內(nèi)存的操作會導致在其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效燎孟。

CPU為了提高處理性能,并不直接和內(nèi)存進行通信尸昧,而是將內(nèi)存的數(shù)據(jù)讀取到內(nèi)部緩存(L1揩页,L2)再進行操作,但操作完并不能確定何時寫回到內(nèi)存烹俗,如果對volatile變量進行寫操作爆侣,當CPU執(zhí)行到Lock前綴指令時,會將這個變量所在緩存行的數(shù)據(jù)寫回到內(nèi)存幢妄,不過還是存在一個問題兔仰,就算內(nèi)存的數(shù)據(jù)是最新的,其它CPU緩存的還是舊值蕉鸳,所以為了保證各個CPU的緩存一致性乎赴,每個CPU通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的數(shù)據(jù)有效性,當發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址的數(shù)據(jù)被修改潮尝,就會將該緩存行設置成無效狀態(tài)榕吼,當CPU讀取該變量時,發(fā)現(xiàn)所在的緩存行被設置為無效勉失,就會重新從內(nèi)存中讀取數(shù)據(jù)到緩存中羹蚣。

END。
我是占小狼乱凿。
在魔都艱苦奮斗顽素,白天是上班族咽弦,晚上是知識服務工作者。
如果讀完覺得有收獲的話胁出,記得關注和點贊哦型型。
非要打賞的話,我也是不會拒絕的划鸽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末输莺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子裸诽,更是在濱河造成了極大的恐慌嫂用,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丈冬,死亡現(xiàn)場離奇詭異嘱函,居然都是意外死亡,警方通過查閱死者的電腦和手機埂蕊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門往弓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蓄氧,你說我怎么就攤上這事函似。” “怎么了喉童?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵撇寞,是天一觀的道長。 經(jīng)常有香客問我堂氯,道長蔑担,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任咽白,我火速辦了婚禮啤握,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晶框。我一直安慰自己排抬,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布三妈。 她就那樣靜靜地躺著畜埋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪畴蒲。 梳的紋絲不亂的頭發(fā)上悠鞍,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音,去河邊找鬼咖祭。 笑死掩宜,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的么翰。 我是一名探鬼主播牺汤,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浩嫌!你這毒婦竟也來了檐迟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤码耐,失蹤者是張志新(化名)和其女友劉穎追迟,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骚腥,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡敦间,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了束铭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廓块。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖契沫,靈堂內(nèi)的尸體忽然破棺而出带猴,到底是詐尸還是另有隱情,我是刑警寧澤懈万,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布浓利,位于F島的核電站,受9級特大地震影響钞速,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嫡秕,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一渴语、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昆咽,春花似錦驾凶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泻轰,卻和暖如春技肩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浮声。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工虚婿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旋奢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓然痊,卻偏偏與公主長得像至朗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子剧浸,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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