關(guān)于關(guān)鍵字Volatile的理解

Java Volatile Keyword

在這篇文章中,我們將關(guān)注Java語言中的基本但經(jīng)常被誤解的概念 - volatile關(guān)鍵字。

1.概述

在Java中悉尾,每個(gè)線程都有一個(gè)獨(dú)立的內(nèi)存空間贺嫂,稱為工作內(nèi)存; 這保存了用于執(zhí)行操作的不同變量的值栏豺。在執(zhí)行操作之后,線程將變量的更新值復(fù)制到主存儲(chǔ)器,并且從那里其他線程可以讀取最新值。

Java volatile關(guān)鍵字作用將Java變量標(biāo)記為“存儲(chǔ)在主存儲(chǔ)器中”絮缅。更確切地說鲁沥,這意味著,每次讀取一個(gè)volatile變量都將從計(jì)算機(jī)的主內(nèi)存中讀取耕魄,而不是從CPU緩存中讀取画恰,并且每次寫入volatile變量都將寫入主內(nèi)存,而不僅僅是CPU緩存吸奴。

簡(jiǎn)單地說允扇,volatile關(guān)鍵字標(biāo)記一個(gè)變量,在多個(gè)線程訪問它的情況下则奥,總是轉(zhuǎn)到主內(nèi)存考润,讀取和寫入。

實(shí)際上读处,自Java 5以來额划,volatile關(guān)鍵字保證的不僅僅是易失性變量被寫入主內(nèi)存并從主內(nèi)存中讀取。我將在以下部分解釋档泽。

可變可見性問題

Java volatile關(guān)鍵字保證可以跨線程查看變量的變化。這可能聽起來有點(diǎn)抽象揖赴,所以讓我詳細(xì)說明馆匿。

在線程操作非易失性變量的多線程應(yīng)用程序中,出于性能原因燥滑,每個(gè)線程可以在處理它們時(shí)將變量從主內(nèi)存復(fù)制到CPU緩存中渐北。如果您的計(jì)算機(jī)包含多個(gè)CPU,則每個(gè)線程可以在不同的CPU上運(yùn)行铭拧。這意味著赃蛛,每個(gè)線程可以將變量復(fù)制到不同CPU的CPU緩存中。這在這里說明:

線程可以保存CPU緩存中主存儲(chǔ)器的變量副本搀菩。

對(duì)于non-volatile變量呕臂,無法保證Java虛擬機(jī)(JVM)何時(shí)將數(shù)據(jù)從主內(nèi)存讀入CPU緩存,或?qū)?shù)據(jù)從CPU緩存寫入主內(nèi)存肪跋。這可能會(huì)導(dǎo)致幾個(gè)問題歧蒋,我將在以下部分中解釋。

想象一下兩個(gè)或多個(gè)線程可以訪問共享對(duì)象的情況州既,該共享對(duì)象包含一個(gè)聲明如下的計(jì)數(shù)器變量:

public class SharedObject {
    public int counter = 0;
}

想象一下谜洽,只有線程1遞增counter變量,但線程1和線程2都可能counter不時(shí)讀取變量吴叶。

如果counter變量未聲明為volatile阐虚,則無法保證何時(shí)將counter變量的值從CPU緩存寫回主內(nèi)存。這意味著counter變量在CPU緩存中的變量值可能與主存儲(chǔ)器中的變量值不同蚌卤。這種情況如下所示:

線程1和主內(nèi)存使用的CPU緩存包含計(jì)數(shù)器變量的不同值实束。

這里的問題是奥秆,其他線程沒有看到counter變量的最新值,原因是它還沒有被另一個(gè)線程寫回主內(nèi)存磕洪,稱為“可見性”問題吭练。其他線程看不到一個(gè)線程的更新。

2.易失性和線程同步

對(duì)于所有多線程應(yīng)用程序析显,我們需要確保一致的行為規(guī)則:

  • 相互排斥 - 一次只有一個(gè)線程執(zhí)行一個(gè)關(guān)鍵部分
  • 可見性 - 一個(gè)線程對(duì)共享數(shù)據(jù)所做的更改對(duì)其他線程可見鲫咽,以維護(hù)數(shù)據(jù)一致性

同步方法和塊提供上述兩種屬性,但代價(jià)是應(yīng)用程序的性能谷异。

Volatile是一個(gè)非常有用的原語分尸,因?yàn)樗梢詭椭?strong>確保數(shù)據(jù)變化的可見性方面,當(dāng)然歹嘹,不提供互斥箩绍。因此,它在我們可以使用多個(gè)線程并行執(zhí)行代碼塊但我們需要確背呱希可見性屬性的地方很有用材蛛。

Java易失性可見性保證

Java volatile關(guān)鍵字旨在解決可變可見性問題。通過聲明counter變量為volatile怎抛,對(duì)counter變量的所有寫操作都將立即寫回內(nèi)存卑吭。此外,counter變量的所有讀取都將直接從主存儲(chǔ)器中讀取马绝。

以下是 變量volatile聲明的counter樣子:

public class SharedObject { 
    public volatile int counter = 0; 
}

聲明volatile變量可以保證對(duì)該變量的其他寫入線程的可見性豆赏。

在上面給出的場(chǎng)景中,一個(gè)線程(T1)修改計(jì)數(shù)器富稻,另一個(gè)線程(T2)讀取計(jì)數(shù)器(但從不修改它)掷邦,聲明該counter變量volatile后足以保證counter變量的寫入對(duì)T2是可見的。

但是椭赋,如果T1和T2都在增加counter變量抚岗,那么聲明 counter變量volatile就不夠了。稍后會(huì)詳細(xì)介紹哪怔。

完全不穩(wěn)定的可見性保證

實(shí)際上苟跪,Java volatile的可見性保證超出了volatile 變量本身。能見度保證如下情形:

如果線程A寫入volatile變量并且線程B隨后讀取相同的volatile變量蔓涧,線程A在寫入之前的所有volatile變量件已,當(dāng)線程B讀取volatile變量后也將對(duì)線程B可見。

如果線程A讀取volatile變量元暴,則讀取變量時(shí)線程A可見的所有變量volatile也將從主存儲(chǔ)器重新讀取篷扩。
讓我用代碼示例說明:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

該udpate()方法寫入三個(gè)變量,其中只有days 是volatile茉盏。

完全volatile可見性保證意味著鉴未,當(dāng)寫入days值時(shí)枢冤,線程可見的所有變量也會(huì)寫入主存儲(chǔ)器。這意味著铜秆,當(dāng)days的值被寫入主內(nèi)存淹真,years和months的值也被寫入主存儲(chǔ)器。

當(dāng)讀取years连茧,months和days的值你可以做這樣的:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

注意totalDays()通過讀取的值的方法開始days到 total變量核蘸。當(dāng)讀取的值days,值months 和years也被讀入到主存儲(chǔ)器中啸驯。因此可以保證看到的最新值days客扎,months并years與上述讀取序列。

指令重新排序挑戰(zhàn)

只要指令的語義含義保持不變罚斗,Java 虛擬機(jī)和CPU就可以出于性能原因重新排序程序中的指令徙鱼。例如,請(qǐng)查看以下說明:

int a = 1;
int b = 2;

a++;
b++;

這些指令可以按以下順序重新排序针姿,而不會(huì)丟失程序的語義含義:`

int a = 1;
a++;

int b = 2;
b++;

然而袱吆,當(dāng)其中一個(gè)變量是volatile變量時(shí),指令重新排序時(shí)將面臨挑戰(zhàn)距淫。讓我們看看MyClass這個(gè)Java volatile教程前面的例子中的類:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦調(diào)用update()方法寫入一個(gè)值到days, 新寫入到y(tǒng)ears 和months的值同樣被寫入到主內(nèi)存杆故。
但是,如果Java VM重新排序指令溉愁,如下所示:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

當(dāng)我們?nèi)バ薷膁ays變量時(shí)months的值和years值仍寫入到主內(nèi)存,但這次發(fā)生的是days修改是發(fā)生在寫months years之前饲趋。新的值(months,years)不能正確被其他線程可見.重新排序的指令的語義含義已經(jīng)改變拐揭。
Java有一個(gè)解決這個(gè)問題的方法,我們將在下一節(jié)中看到奕塑。

3.發(fā)生在保證之前

從Java 5開始堂污,volatile關(guān)鍵字還提供了額外的功能,可確保包括非易失性變量在內(nèi)的所有變量的值與Volatile寫操作一起寫入主存儲(chǔ)器龄砰。

這稱為Happens-Before盟猖,因?yàn)樗鼮樗凶兞刻峁┝藢?duì)另一個(gè)讀取線程的可見性。此外换棚,JVM不會(huì)重新排序volatile變量的讀寫指令式镐。

Java volatile Happens-Before Guarantee

為了解決指令重新排序挑戰(zhàn),volatile除了可見性保證之外固蚤,Java 關(guān)鍵字還提供“先發(fā)生”保證娘汞。事先發(fā)生的保證保證:
1.如果讀取/寫入最初發(fā)生在對(duì)volatile變量寫入之前,則無法重新排序?qū)ζ渌兞康淖x和寫操作夕玩。

對(duì)volatile變量的寫入之前的讀/寫將會(huì)保證在寫入”volatile"之前“先發(fā)生”你弦。
請(qǐng)注意惊豺,在寫入“volatile"變量之前,可能會(huì)對(duì)其他變量的讀/寫進(jìn)行重新排序,以使其在寫入“volatile"后發(fā)生禽作。只是不是另一種方式尸昧。從以后到以前是允許的, 但從以前到以后是不允許的。

2.如果讀取/寫入最初發(fā)生在對(duì)volatile變量讀取之后旷偿,則無法重新排序?qū)ζ渌兞康淖x和寫操作烹俗。

請(qǐng)注意, 在讀取“volatile"變量之前, 可能會(huì)對(duì)其他變量的讀取進(jìn)行重新排序, 以使其在讀取“volatile"后發(fā)生。只是不是另一種方式狸捅。從之前到以后是允許的, 但從以后到以前是不允許的衷蜓。

上述情況-在保證確保volatile關(guān)鍵字的可見性保證被強(qiáng)制執(zhí)行之前。

volatile is Not Always Enough

即使volatile關(guān)鍵字保證volatile變量直接從主存儲(chǔ)器讀取變量的所有讀取尘喝,并且所有對(duì)volatile變量的寫入都直接寫入主存儲(chǔ)器磁浇,仍然存在聲明變量不足的情況volatile

在前面解釋的情況中朽褪,只有線程1寫入共享counter變量置吓,聲明該counter變量volatile足以確保線程2始終看到最新的寫入值。

實(shí)際上缔赠,volatile如果寫入變量的新值不依賴于其先前的值衍锚,則多個(gè)線程甚至可以寫入共享變量,并且仍然具有存儲(chǔ)在主存儲(chǔ)器中的正確值嗤堰。換句話說戴质,如果將值寫入共享volatile變量的線程首先不需要讀取其值來計(jì)算其下一個(gè)值。

一旦線程需要首先讀取volatile變量的值踢匣,并且基于該值為共享volatile變量生成新值告匠,volatile變量就不再足以保證正確的可見性。讀取volatile 變量和寫入新值之間的短時(shí)間間隔會(huì)產(chǎn)生競(jìng)爭(zhēng)條件 离唬,其中多個(gè)線程可能讀取volatile變量的相同值后专,為變量生成新值,并在將值寫回時(shí)主存 - 覆蓋彼此的值输莺。

多個(gè)線程遞增相同計(jì)數(shù)器的情況恰好是 volatile變量不夠的情況戚哎。以下部分更詳細(xì)地解釋了這種情況。

想象一下嫂用,如果線程1將counter值為0 的共享變量讀入其CPU高速緩存型凳,則將其增加到1并且不將更改的值寫回主存儲(chǔ)器。然后嘱函,線程2可以counter從主存儲(chǔ)器讀取相同的變量啰脚,其中變量的值仍為0,進(jìn)入其自己的CPU高速緩存。然后橄浓,線程2也可以將計(jì)數(shù)器遞增到1粒梦,也不將其寫回主存儲(chǔ)器。這種情況如下圖所示:

兩個(gè)線程已將共享計(jì)數(shù)器變量讀入其本地CPU高速緩存并遞增荸实。

線程1和線程2現(xiàn)在幾乎不同步匀们。共享counter變量的實(shí)際值應(yīng)為2,但每個(gè)線程的CPU緩存中的變量值為1准给,而主存中的值仍為0.這是一個(gè)混亂泄朴!即使線程最終將共享counter變量的值寫回主存儲(chǔ)器,該值也將是錯(cuò)誤的露氮。

什么時(shí)候揮發(fā)夠了祖灰?

正如我前面提到的,如果兩個(gè)線程都在讀取和寫入共享變量畔规,那么使用 volatile關(guān)鍵字是不夠的局扶。 在這種情況下,您需要使用synchronized來保證變量的讀取和寫入是原子的叁扫。讀取或?qū)懭雟olatile變量不會(huì)阻止線程讀取或?qū)懭肴琛榇耍仨?code>synchronized 在關(guān)鍵部分代碼

作為synchronized塊的替代方法莫绣,您還可以使用java.util.concurrent包中提供的原子數(shù)據(jù)類型畴蒲。例如,AtomicLong或者 AtomicReference來避免競(jìng)爭(zhēng)條件对室。

  • 標(biāo)記為synchronized的邏輯變?yōu)橥綁K模燥,在任何給定時(shí)間只允許一個(gè)線程執(zhí)行。

如果只有一個(gè)線程讀取和寫入volatile變量的值掩宜,而其他線程只讀取變量蔫骂,那么讀取線程將保證看到寫入volatile變量的最新值。如果不使變量變?yōu)関olatile锭亏,則無法保證。

volatile關(guān)鍵字保證適用于32位和64位變量硬鞍。

揮發(fā)性的性能考慮因素

讀取和寫入volatile變量會(huì)導(dǎo)致變量被讀取或?qū)懭胫鞔鎯?chǔ)器慧瘤。讀取和寫入主內(nèi)存比訪問CPU緩存更昂貴。訪問volatile變量也會(huì)阻止指令重新排序固该,這是一種正常的性能增強(qiáng)技術(shù)锅减。因此,在真正需要強(qiáng)制實(shí)施變量可見性時(shí)伐坏,應(yīng)該只使用volatile變量怔匣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子每瞒,更是在濱河造成了極大的恐慌金闽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剿骨,死亡現(xiàn)場(chǎng)離奇詭異代芜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)浓利,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門挤庇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贷掖,你說我怎么就攤上這事嫡秕。” “怎么了苹威?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵昆咽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我屠升,道長(zhǎng)潮改,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任腹暖,我火速辦了婚禮汇在,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脏答。我一直安慰自己糕殉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布殖告。 她就那樣靜靜地躺著阿蝶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪黄绩。 梳的紋絲不亂的頭發(fā)上羡洁,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音爽丹,去河邊找鬼筑煮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛粤蝎,可吹牛的內(nèi)容都是我干的真仲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼初澎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼秸应!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤软啼,失蹤者是張志新(化名)和其女友劉穎桑谍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焰宣,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霉囚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匕积。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盈罐。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖闪唆,靈堂內(nèi)的尸體忽然破棺而出盅粪,到底是詐尸還是另有隱情,我是刑警寧澤悄蕾,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布票顾,位于F島的核電站,受9級(jí)特大地震影響帆调,放射性物質(zhì)發(fā)生泄漏奠骄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一番刊、第九天 我趴在偏房一處隱蔽的房頂上張望含鳞。 院中可真熱鬧,春花似錦芹务、人聲如沸蝉绷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽熔吗。三九已至,卻和暖如春佳晶,著一層夾襖步出監(jiān)牢的瞬間桅狠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工轿秧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留中跌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓淤刃,卻偏偏與公主長(zhǎng)得像晒他,于是被迫代替她去往敵國(guó)和親吱型。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逸贾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

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