深入理解Java多線程中的volatile關(guān)鍵字

  • Java 的 volatile關(guān)鍵字對(duì)可見性的保證
  • Java 的 volatile關(guān)鍵字在保證可見性之前的所做的事情
  • 為什么volatile關(guān)鍵字有時(shí)候也不是足夠的
  • 什么時(shí)候volatile足夠了
  • volatile關(guān)鍵字對(duì)效率的影響

Java關(guān)鍵字用于將一個(gè)變量標(biāo)記為“存儲(chǔ)在內(nèi)存中的變量”擦俐。更準(zhǔn)確的說(shuō)拨与,意思就是每一次對(duì)volatile標(biāo)記的變量進(jìn)行讀取的時(shí)候侮繁,都是直接從電腦的主內(nèi)存進(jìn)行的,而不是從cpu的cache中您访,而且每個(gè)對(duì)volatile變量的寫入操作剥险,都會(huì)被直接寫入到主存里纺涤,而不是只寫到cache里五垮。

實(shí)際上睬棚,從java5開始琳疏,volatile關(guān)鍵字就不僅僅是保證volatile變量從主存讀寫有决,筆者會(huì)在后面詳細(xì)討論這個(gè)問(wèn)題。

Java 的 volatile關(guān)鍵字對(duì)可見性的保證

Java的volatile關(guān)鍵字可以保證變量的可見性空盼。說(shuō)起來(lái)很簡(jiǎn)單书幕,但具體是什么意思呢?

在多線程的應(yīng)用程序中揽趾,線程操作非volatile的變量台汇,為了更快速的執(zhí)行程序,每個(gè)線程都會(huì)將變量從主存復(fù)制到cpu的cache中篱瞎。如果你的電腦有多個(gè)cpu苟呐,每個(gè)線程都在不同的cpu上運(yùn)行,這就意味著俐筋,每個(gè)線程將變量的值復(fù)制到不同的cpu的cache上牵素,就像下面這個(gè)圖所表明:

Paste_Image.png

如果變量沒(méi)有聲明為volatile,那么就無(wú)法知道澄者,變量什么時(shí)候從主存中讀取到cpu的cache中笆呆,有什么時(shí)候從cache中寫回到主存中。這就可能造成很多潛在的問(wèn)題:

假設(shè)一種情況粱挡,多個(gè)線程同時(shí)持有一個(gè)共享對(duì)象的引用赠幕,這個(gè)對(duì)象包括一個(gè)counter變量:

public class SharedObject {

    public int counter = 0;

}

假設(shè)這種情況,只有線程1自增了這個(gè)counter變量询筏,但是線程1和線程2可能隨時(shí)讀取這個(gè)counter變量榕堰。如果這個(gè)counter變量沒(méi)有被聲明為volatile,那么就無(wú)法確認(rèn)嫌套,什么時(shí)候counter的變量的值會(huì)從cpu的cache中寫回到主存中逆屡,這就意味著,counter變量的值在cpu的cache中的值可能和主存中不一樣踱讨,如下圖所示:

Paste_Image.png

這個(gè)線程的問(wèn)題無(wú)法及時(shí)的看到變量的最新的值魏蔗,因?yàn)榭赡苓@個(gè)變量還沒(méi)有被另一個(gè)線程寫回到主存中。所以一個(gè)線程對(duì)一個(gè)變量的更新對(duì)其他的線程是不可見的勇蝙。這就是我們最初提出的線程的可見性問(wèn)題沫勿。

通過(guò)將一個(gè)變量聲明為volatile挨约,那么所有對(duì)這個(gè)變量寫操作會(huì)被直接寫回到主內(nèi)存中味混,所以這對(duì)線程都是可見的。而且诫惭,所有對(duì)這個(gè)變量的讀取操作翁锡,也會(huì)直接從主存中讀取,下面說(shuō)明了如何聲明一個(gè)voaltile變量:

public class SharedObject {

    public volatile int counter = 0;

}

** 將一個(gè)變量聲明為volatile就可以保證寫操作夕土,其他線程對(duì)這個(gè)變量的可見性 **

Java 的 volatile關(guān)鍵字在保證可見性之前的所做的事情

從java5開始馆衔,volatile關(guān)鍵字不僅可以保證變量直接從主內(nèi)存中讀取瘟判,還有一下作用:

  • 如果線程A對(duì)一個(gè)volatile變量進(jìn)行寫操作,線程B隨后讀取同一個(gè)volatile值角溃,那么在線程將變量寫操作完成之后的所有變量對(duì)線程A和B都是可見的拷获。
  • 那些操作volatile變量的讀寫指令的順序無(wú)法被JVM改變(JVM有時(shí)候?yàn)榱诵蕰?huì)改變變量讀寫順序,只要JVM判斷改變順序?qū)Τ绦驔](méi)有影響的話)减细。

上面兩段話不是很理解匆瓜,我們接下來(lái)進(jìn)行一個(gè)更細(xì)致的說(shuō)明:

當(dāng)一個(gè)線程對(duì)一個(gè)volatile變量進(jìn)行寫操作的時(shí)候,不僅僅是這個(gè)變量自己被寫入到主存中未蝌,同時(shí)驮吱,其他所有在這之前被改變值的變量也都會(huì)線程先寫入到主存中。
當(dāng)一個(gè)線程對(duì)一個(gè)volatile變量進(jìn)行讀取操作萧吠,他也會(huì)將所有跟著那個(gè)volatile變量一起寫入到主存中的其他所有變量一起讀出來(lái)左冬。
看下面這個(gè)例子:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;

因?yàn)榫€程A在對(duì)volatile的sharedObject.counter進(jìn)行寫操作之前,先對(duì)sharedObject.nonVolatile變量進(jìn)行寫操作纸型,所以當(dāng)線程A要將volatile的sharedObject.counter寫回到主存時(shí)拇砰,這兩個(gè)變量都會(huì)被寫回到主存中。

同理绊袋,線程B在讀取volatile變量到sharedObject.counter的時(shí)候毕匀,兩個(gè)變量sharedObject.counter and sharedObject.nonVolatile所以線程讀取變量sharedObject.nonVolatile就會(huì)看到他被線程A改變后的值。

開發(fā)者可以利用這個(gè)擴(kuò)展的可見性去放大線程間的變量可見性癌别,不需要將每一個(gè)變量都聲明為volatile皂岔,只需要聲明一兩個(gè)變量為volatile就可以了。下面這個(gè)簡(jiǎn)單的例子展姐,就來(lái)說(shuō)明這個(gè)問(wèn)題:

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}

線程A可能會(huì)調(diào)用put方法將objects put進(jìn)去躁垛,線程B可能會(huì)調(diào)用take方法將object拿出來(lái)。這個(gè)類可以正常工作圾笨,只要我們使用一個(gè)volatile變量即可(不使用同步語(yǔ)句)教馆,只要只有線程A調(diào)用put,只有線程B調(diào)用take擂达。

然后土铺,JVM有時(shí)候?yàn)榱颂岣咝剩赡軙?huì)改變指令執(zhí)行的順序板鬓,只要JVM判斷這樣做不改變指令的語(yǔ)義悲敷,那么就有可能改變指令的順序。那么如果JVM改變了指令的執(zhí)行順序會(huì)發(fā)生什么呢俭令?put方法可能會(huì)像下面這樣執(zhí)行:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

我們觀察到后德,現(xiàn)在對(duì)于volatile的hasNewObject 操作在object = newObject;之前執(zhí)行,這說(shuō)明抄腔,object還沒(méi)有真正被賦值新對(duì)象瓢湃,但是hasNewObject 已經(jīng)先變?yōu)閠rue了理张。對(duì)于JVM來(lái)說(shuō),這種交換是完全有可能的绵患。因?yàn)檫@兩個(gè)write的指令彼此不是互相依賴的雾叭。

但是這樣交換順序之后可能會(huì)對(duì)object變量的可見性產(chǎn)生不好的影響。首先落蝙,線程B可能會(huì)在線程A真正給object寫入一個(gè)新值之前拷况,就看到hasNewObject 變?yōu)閠rue。
另一方面掘殴,我們無(wú)法確保object什么時(shí)候會(huì)被真正寫入到主內(nèi)存中赚瘦。

為了防止上面這種情況的發(fā)生,volatile關(guān)鍵字就提出了一種“happens before guarantee”奏寨,這可以保證volatile的變量的讀寫指令不會(huì)被重新排序起意。指令前面的和后面的可以隨意排序,但是volatile變量的讀寫指令的相對(duì)順序是不能改變的病瞳。

看下面這個(gè)例子就能理解了:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;

JVM可能會(huì)改變前三個(gè)指令的順序揽咕,只要他們?cè)趘olatile的寫指令之前發(fā)生(就是說(shuō)他們必須在volatile的寫指令之前發(fā)生)。
同理套菜,JVM也可能改變后三個(gè)指令的順序亲善,只要他們?cè)趘olatile的寫指令之后發(fā)生。

這就是對(duì)于Java的 volatile happens before guarantee.的最基本的理解

Volatile有時(shí)候也是不夠的

雖然volatile可以保證讀取操作直接從主內(nèi)存中的讀取逗柴,寫操作直接寫到內(nèi)存中蛹头,但仍然存在一些情況下,光使用volatile關(guān)鍵字是不夠的戏溺。

在之前的舉例的程序中渣蜗,只有一個(gè)線程在向共享變量寫入數(shù)據(jù)的時(shí)候,聲明為volatile旷祸,另一個(gè)線程就可以一直看到最新被寫入的值耕拷。

實(shí)際上,只要新值不依賴舊值的情況下托享,多個(gè)線程同時(shí)向共享的volatile變量里寫入數(shù)據(jù)時(shí)骚烧,仍然能在主內(nèi)存中得到正確的值。換句話說(shuō)闰围,就是這個(gè)volatile變量值更新的時(shí)候赃绊,不需要先讀取出他以前的值才能得到下一個(gè)值。

只要一個(gè)線程需要先讀取一個(gè)voaltile變量辫诅,然后必須基于他的值才能產(chǎn)生新的值凭戴,那么volatile關(guān)鍵字就不再能保證變量的可見性了涧狮。在讀取變量和寫入變量的時(shí)候炕矮,存在一個(gè)短的時(shí)間間隙么夫,這就會(huì)造成,多個(gè)線程可能會(huì)在這個(gè)間隙讀取同一個(gè)值肤视,產(chǎn)生一個(gè)新值档痪,然后寫入到主內(nèi)存中,將其他線程對(duì)值的改變給覆蓋了邢滑。

所以常見的情況就是如果一個(gè)volatile變量在進(jìn)行自增或者自減操作腐螟,那么這時(shí)候使用volatile就可能出問(wèn)題。
接下來(lái)我們更深入的討論這個(gè)問(wèn)題困后,假設(shè)線程1讀取一個(gè)共享的counter變量到cpu的cache中乐纸,此時(shí)他的值是0,然后給它自增加一摇予,但是還沒(méi)有寫到主存中汽绢,所以主存中還是1,線程2也能夠讀取同一個(gè)counter變量侧戴,而這個(gè)變量讀取的時(shí)候還是0宁昭,在他自己的cpucache中,這樣就出現(xiàn)問(wèn)題了:

Paste_Image.png

線程1和線程2實(shí)際上是不同步的酗宋。共享變量counter的真實(shí)值實(shí)際上應(yīng)該為2积仗,因?yàn)楸患恿藘纱危敲總€(gè)線程在自己的cache上存的值是1蜕猫,而且在主存中這個(gè)值仍然是0寂曹,這就變得很混亂。即使線程最后將值寫回到主存中回右,但最后的值也是不正確的稀颁。

什么時(shí)候volatile足夠了

前文中提到,如果兩個(gè)線程都在對(duì)volatile變量進(jìn)行讀寫操作楣黍,那么僅僅使用volatile關(guān)鍵字是遠(yuǎn)遠(yuǎn)不夠的匾灶。你需要使用synchronize關(guān)鍵字,來(lái)保證讀寫操作的原子性租漂。
但如果是只有一個(gè)線程在讀寫volatile變量阶女,另外的多個(gè)線程僅僅是讀取這個(gè)變量的話,那么這就可以保證哩治,其他讀線程所看到的變量值都是最新的秃踩。volatile關(guān)鍵字可以使用在32位或者64位的變量上。

volatile關(guān)鍵字對(duì)效率的影響

讀寫一個(gè)volatile變量的時(shí)候业筏,會(huì)導(dǎo)致變量直接在主存中讀寫憔杨,顯然,直接從主存中讀寫速度要比從cache中來(lái)得慢蒜胖。另一方面消别,操作volatile變量的時(shí)候不能改變指令的執(zhí)行順序抛蚤,這一定程度上也會(huì)影響讀寫的效率。所以寻狂,只有我們需要確保變量可見性的時(shí)候岁经,才會(huì)使用volatile關(guān)鍵字。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛇券,一起剝皮案震驚了整個(gè)濱河市缀壤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纠亚,老刑警劉巖塘慕,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蒂胞,居然都是意外死亡苍糠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門啤誊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)岳瞭,“玉大人,你說(shuō)我怎么就攤上這事蚊锹⊥ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵牡昆,是天一觀的道長(zhǎng)姚炕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)丢烘,這世上最難降的妖魔是什么柱宦? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮播瞳,結(jié)果婚禮上掸刊,老公的妹妹穿的比我還像新娘。我一直安慰自己赢乓,他們只是感情好忧侧,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著牌芋,像睡著了一般蚓炬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上躺屁,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天肯夏,我揣著相機(jī)與錄音,去河邊找鬼。 笑死驯击,一個(gè)胖子當(dāng)著我的面吹牛烁兰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播余耽,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼苹熏!你這毒婦竟也來(lái)了碟贾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤轨域,失蹤者是張志新(化名)和其女友劉穎袱耽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體干发,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡朱巨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了枉长。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冀续。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖必峰,靈堂內(nèi)的尸體忽然破棺而出洪唐,到底是詐尸還是另有隱情,我是刑警寧澤吼蚁,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布凭需,位于F島的核電站,受9級(jí)特大地震影響肝匆,放射性物質(zhì)發(fā)生泄漏粒蜈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一旗国、第九天 我趴在偏房一處隱蔽的房頂上張望枯怖。 院中可真熱鬧,春花似錦能曾、人聲如沸嫁怀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)塘淑。三九已至,卻和暖如春蚂斤,著一層夾襖步出監(jiān)牢的瞬間存捺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捌治,地道東北人岗钩。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像肖油,于是被迫代替她去往敵國(guó)和親兼吓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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

  • 前言 今天介紹下volatile關(guān)鍵字森枪,volatile這個(gè)關(guān)鍵字可能很多朋友都聽說(shuō)過(guò)视搏,或許也都用過(guò)。在Java ...
    嘟爺MD閱讀 1,296評(píng)論 7 27
  • Java的關(guān)鍵字 volatile 用于將變量標(biāo)記為“存儲(chǔ)于主內(nèi)存中”县袱。更確切地說(shuō)浑娜,對(duì) volatile 變量的每...
    holysu閱讀 5,793評(píng)論 0 13
  • 《體驗(yàn)》 送葬的隊(duì)伍從東到西 在坡馬的中心路上走過(guò)去 百家姓上的張、王式散、李筋遭、趙…… 都在走過(guò)去。我感到路在縮短 因...
    溪小石吳索衛(wèi)閱讀 92評(píng)論 1 1
  • 一 奧莉加·伊凡諾夫娜所有的朋友和熟人都出席了她的婚禮暴拄。 “你們瞧瞧:他是不是有點(diǎn)意思漓滔?”她對(duì)朋友們說(shuō),朝丈夫那邊...
    小團(tuán)閱讀 784評(píng)論 0 0
  • 1.2016年中讓自己可以拿到機(jī)動(dòng)車駕駛證(得花時(shí)間努力去練車) 2.閱讀1000本電子書(希望更好的提升自己) ...
    兔子彤閱讀 626評(píng)論 0 1