volatile關(guān)鍵字詳解

一垮耳、volatile關(guān)鍵字簡介

synchronized關(guān)鍵字是阻塞式同步乞榨,在線程競爭激烈的時(shí)候會(huì)逐漸由偏向鎖膨脹為重量級(jí)鎖。而volatile是JVM提供的最輕量級(jí)的同步機(jī)制因谎。JMM告訴我們各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存按声,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理菱阵。不過線程在工作內(nèi)存中進(jìn)行操作后將會(huì)何時(shí)寫入主內(nèi)存中拯坟?這個(gè)時(shí)機(jī)普通機(jī)制是沒有規(guī)定的晤碘。

volatile一般用于修飾會(huì)被不同線程訪問和修改的變量衍锚,而針對(duì)volatile修飾的變量給JVM給了規(guī)定:線程對(duì)volatile變量的修改會(huì)立刻被其他線程感知友题,即被volatile修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,這樣就不會(huì)出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象构拳,保證了數(shù)據(jù)的可見性咆爽。

volatile具有可見性和有序性

二、volatile實(shí)現(xiàn)原理

加入volatile關(guān)鍵字的代碼的class字節(jié)碼中會(huì)多出了一個(gè)lock前綴指令置森,lock指令相當(dāng)于一個(gè)內(nèi)存屏障斗埂,主要做了三件事:

  1. 重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
  2. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存
  3. 這個(gè)寫回內(nèi)存的操作會(huì)使其他CPU里緩存的該內(nèi)存地址的數(shù)據(jù)無效,即新寫入的值對(duì)別的線程可見

經(jīng)過這一波操作后凫海,其他的線程發(fā)現(xiàn)自己工作內(nèi)存中的緩存失效后呛凶,就會(huì)從內(nèi)存中重新讀取該變量數(shù)據(jù),即保證了其他線程可以獲取當(dāng)前最新值行贪。即可以說volatile實(shí)現(xiàn)了緩存一致性協(xié)議:每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存的值是不是過期了漾稀,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改模闲,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候崭捍,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里尸折。

三、volatile的happens-before關(guān)系和內(nèi)存語義分析

在之前JMM一文中對(duì)happens-before規(guī)則介紹殷蛇,有一條是:對(duì)一個(gè)volatile域的寫实夹,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀

來看一段代碼

class VolatileExam{
    private int a = 0;
    private volatile boolean flag = false;

    public void writer() {
        a = 1;               //1
        flag = true;         //2
    }

    public void reader() {
        if (flag) {          //3
            int i = a;       //4
        }
    }
}

對(duì)volatile的happens-before分析:

  • 線程1先執(zhí)行writer方法粒梦,然后線程2執(zhí)行reader方法亮航。
  • 我們由happens-before規(guī)則推知,2 happens-before 3 (volatile變量的寫happens-before于任意后續(xù)對(duì)volatile變量的讀)
  • 由傳遞性可以得知1 happens-before 4
  • 由happens-before規(guī)則:如果A happens-before B匀们,則A的執(zhí)行結(jié)果對(duì)B可見缴淋,且A的執(zhí)行順序先于B的執(zhí)行順序
  • 那么2的執(zhí)行結(jié)果對(duì)3可見,也就是說線程1將flag修改為true泄朴,線程2能夠迅速感知

volatile的內(nèi)存語義分析:

  • 如果線程1先進(jìn)行writer方法重抖,隨后線程2進(jìn)行reader方法。一開始的本地都是a和flag的初始化狀態(tài)
  • 在線程1線程2的本地內(nèi)存里叼旋,線程1對(duì)初始值進(jìn)行了修改并寫入主內(nèi)存中仇哆,而在線程2的本地內(nèi)存里還是原來的值
  • 由于volatile變量寫后,線程中本地內(nèi)存中共享變量就會(huì)置為失效狀態(tài)夫植,因此線程2需要再次從主內(nèi)存中讀取最新的共享變量值讹剔。從橫向看,線程1和線程2進(jìn)行了通信详民,線程1在寫volatile變量的時(shí)候告訴線程2:你的本地內(nèi)存中的值是舊的
  • 線程2在讀取volatile變量的時(shí)候就被告知目前自己的本地值是舊的延欠,那線程2就只能去主內(nèi)存中去取最新值了

四、volatile內(nèi)存語義的具體實(shí)現(xiàn)

為了性能優(yōu)化沈跨,JMM在不改變正確語義的前提下由捎,會(huì)允許編譯器和處理器對(duì)指令序列進(jìn)行重排序,那如果想阻止重排序就得添加內(nèi)存屏障饿凛。


在這里插入圖片描述

四個(gè)內(nèi)存屏障:

屏障類型 指令類型 說明
LoadLoadBarriers Load1狞玛;LoadLoad;Load2 確保Load1的數(shù)據(jù)的裝載先于Load2及所有后續(xù)裝載指令的裝載
StoreStoreBarriers Store1涧窒;StoreStore心肪;Store2 確保Store1數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存)先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ)
LoadStoreBarriers Load1;LoadStore纠吴;Store2 確保Load1的數(shù)據(jù)的裝載先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ)
StoreLoadBarriers Store1硬鞍;StoreLoad;Load2 確保Store1的數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存)先于Load2及所有后續(xù)的裝載指令的裝載

為了實(shí)現(xiàn)volatile內(nèi)存語義,編譯器在生成字節(jié)碼時(shí)固该,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序:

  1. 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障
  2. 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障
  3. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
  4. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障

StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序锅减;
StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序

五、volatile如何保證內(nèi)存可見性

復(fù)習(xí)一下JMM的8種原子操作:

  1. lock(鎖定):作用于主內(nèi)存中的變量伐坏,把一個(gè)變量表示為一個(gè)線程獨(dú)占的狀態(tài)
  2. unlock(解鎖):作用于主內(nèi)存中的變量怔匣,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定
  3. read(讀戎):作用于主內(nèi)存的變量劫狠,把一個(gè)變量的值從主內(nèi)存讀取到線程的工作內(nèi)存中拴疤,以便于后面的load操作
  4. load(載入):作用于工作內(nèi)存中的變量永部,把read操作從主存中得到的變量值放入工作內(nèi)存中的變量副本
  5. use(使用):作用于工作內(nèi)存中的變量,把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎呐矾,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作
  6. assign(賦值):作用于工作內(nèi)存中的變量苔埋,把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)就執(zhí)行這個(gè)操作
  7. store(存儲(chǔ)):作用于工作內(nèi)存中的變量蜒犯,把工作內(nèi)存中一個(gè)變量的值傳送給主存中以便于后面的write操作
  8. write(寫入):作用于主內(nèi)存中的變量组橄,把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中

volatile規(guī)定:read、load罚随、use動(dòng)作必須連續(xù)出現(xiàn)玉工;assign、store淘菩、write動(dòng)作必須連續(xù)出現(xiàn)
所以volatile保證:每次讀取前必須先從主內(nèi)存刷新最新的值遵班,每次寫入后必須立即同步回主內(nèi)存當(dāng)中。即volatile關(guān)鍵字修飾的變量看到的隨時(shí)是自己的最新值潮改。

六狭郑、volatile的注意事項(xiàng)

public class VolatileExample {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        //開十個(gè)線程,讓他們每個(gè)都自增10000次汇在,理論上應(yīng)該得到10000
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

多次運(yùn)行看出翰萨,每次都得不到10000,這說明volatile并不能保證整體原子性糕殉。問題就是counter++并不是一個(gè)原子操作亩鬼,他包含了三個(gè)步驟:

  1. 讀取counter的值
  2. 對(duì)counter+1
  3. 將新的值賦給變量counter

這么看來如果線程1讀取counter到工作內(nèi)存后,其他線程對(duì)這個(gè)值已經(jīng)做了自增操作阿蝶,那么線程A的這個(gè)值自然就是一個(gè)過期的值雳锋,造成了數(shù)據(jù)的臟讀,因此結(jié)果必然小于10000赡磅。

如果想讓volatile保證整體原子性魄缚,必須符合:

  1. 運(yùn)算結(jié)果不依賴變量的當(dāng)前值,或者能夠確保只有一個(gè)線程修改變量的值;
  2. 變量不需要與其他的狀態(tài)變量共同參與不變約束

如果編譯器經(jīng)過分析后冶匹,認(rèn)定一個(gè)volatile變量只會(huì)被單個(gè)線程訪問习劫,那么編譯器可以把這個(gè)volatile變量當(dāng)做一個(gè)普通的變量來對(duì)待。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嚼隘,一起剝皮案震驚了整個(gè)濱河市诽里,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌飞蛹,老刑警劉巖谤狡,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卧檐,居然都是意外死亡墓懂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門霉囚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捕仔,“玉大人,你說我怎么就攤上這事盈罐“竦” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵盅粪,是天一觀的道長钓葫。 經(jīng)常有香客問我,道長票顾,這世上最難降的妖魔是什么础浮? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮库物,結(jié)果婚禮上霸旗,老公的妹妹穿的比我還像新娘。我一直安慰自己戚揭,他們只是感情好诱告,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著民晒,像睡著了一般精居。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上潜必,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天靴姿,我揣著相機(jī)與錄音,去河邊找鬼磁滚。 笑死佛吓,一個(gè)胖子當(dāng)著我的面吹牛宵晚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播维雇,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼淤刃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吱型?” 一聲冷哼從身側(cè)響起逸贾,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎津滞,沒想到半個(gè)月后铝侵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡触徐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年咪鲜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锌介。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嗜诀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孔祸,到底是詐尸還是另有隱情,我是刑警寧澤发皿,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布崔慧,位于F島的核電站,受9級(jí)特大地震影響穴墅,放射性物質(zhì)發(fā)生泄漏惶室。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一玄货、第九天 我趴在偏房一處隱蔽的房頂上張望皇钞。 院中可真熱鬧,春花似錦松捉、人聲如沸夹界。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽可柿。三九已至,卻和暖如春丙者,著一層夾襖步出監(jiān)牢的瞬間复斥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工械媒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留目锭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像痢虹,于是被迫代替她去往敵國和親键俱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355