Java中的Volatile關(guān)鍵字

Java的volatile關(guān)鍵字用于標記一個Java變量為“在主存中存儲”。更確切的說悍汛,對volatile變量的讀取會從計算機的主存中讀取,而不是從CPU緩存中讀取澎怒,對volatile變量的寫入會寫入到主存中杭抠,而不只是寫入到CPU緩存脸甘。

實際上,從Java5開始祈争,volatile關(guān)鍵字不只是保證了volatile變量在主存中寫入和讀取斤程,我回在后面的部分做相關(guān)的解釋。

變量可見性問題

Java的volatile關(guān)鍵字保證了多個線程對變量值變化的可見性菩混。這聽起來有點抽象忿墅,讓我來詳細解釋。

在一個多線程的程序中沮峡,當多個線程操作非volatile變量時疚脐,出于性能原因,每個線程會從主存中拷貝一份變量副本到一個CPU緩存中邢疙。如果你的計算機有多于一個CPU棍弄,每個線程可能會在不同的CPU中運行。這意味著每個簡稱拷貝變量到不同CPU的緩存中疟游,如下圖:

對于非volatile變量呼畸,并沒有保證何時JVM從主存中讀取數(shù)據(jù)到CPU緩存,或者從CPU緩存中寫出數(shù)據(jù)到主存颁虐。這會導致一些問題蛮原。

想象一種情況,多于一個線程訪問一個共享對象另绩,這個共享對象包含一個計數(shù)變量如下聲明:

publicclassShareObject{publicintcounter =0;}

考慮只有一個線程Thread1增加counter這個變量的值儒陨,但是Tread1和Thread2可能有時會讀取counter變量。

如果counter變量沒有被聲明為volatile笋籽,就不能保證何時這個變量的值會從CPU緩存寫回主存蹦漠,這意味著,在CPU緩存中的counter變量的值可能和主存中的不一樣车海。如下圖所示:

線程沒有看到一個變量最新更新的值的原因是這個變量還沒有被一個線程寫回到主存笛园,這被稱為“可見性”問題。一個線程對變量的更新對其他線程不可見侍芝。

Java的volatile可見性保證

Java的volatile關(guān)鍵字想要解決變量可見性問題喘沿。通過聲明counter變量為volatile,所有對counter變量的寫入都回立即寫回到主存竭贩,同時所有對counter變量也都會從主存中讀取。

西面的代碼展示了如何把counter變量聲明為volatile:

publicclassSharedObject{publicvolatileintcounter =0;}

聲明一個變量為volatile保證了對變量的寫入對其他線程的可見性莺禁。

在上面的場景中留量,一個線程(T1)修改了counter變量的值,另一個線程(T2)讀取counter變量(但是不修改它),聲明counter變量為volatile足以保證對counter變量的寫入對T2可見楼熄。

但是忆绰,如果T1和T2都去增加counter變量的只,name聲明counter變量為volatile是不夠的可岂,后面會說明错敢。

全volatile可見性保證

實際上,Java的volatile的可見性保證不止volatile變量本身缕粹≈擅可見性保證如下:

如果線程A寫一個volatile變量,線程B隨后讀取這個volatile變量平斩,那么在寫這個volatile變量之前對線程A可見的所有變量亚享,在線程B讀取這個volatile變量之后對線程B也可見。

如果線程A讀取一個volatile變量绘面,那么當A讀取這個volatile變量時所有對線程A可見的變量也可以從主存中再次讀取欺税。

我用下面的代碼來說明:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

update()方法寫入三個變量,只有days變量是volatile的揭璃。

全volatile可見性保證的意思是晚凿,當一個值寫入到days變量,則所有對當前線程可見的變量也會都寫入到主存瘦馍,也就是當一個值寫入到days變量歼秽,則years和months的只也被寫入到主存。

當讀取years扣墩,months和days的值哲银,可以這樣做:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicinttotalDays(){inttotal =this.days; total += months *30; total += years *365;returntotal; }publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

需要注意的是totalDays()方法起始于讀取days的值到total變量中。當讀取days的值時呻惕,months和years的值也被讀取到主存嚣艇。因此可以保證你看到的是days歧斟,months和years的最新的值,前提是保證上面的讀取順序。

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

出于性能的考量穿剖,JVM和CPU允許對程序中的指令進行重排序,只要指令的語義不變佑笋。例如下面的指令:

int a =1;intb =2;a++;b++;

這些指令可以按照下面的順序重排寄狼,并不會丟失程序的語義:

int a =1;a++;intb =2;b++;

但是,指令重排序?qū)τ谄渲幸粋€變量是volatile變量這種情況是有挑戰(zhàn)的柑营。讓我們看一下MyClass這個類:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

一旦update()方法對days變量寫入一個值屈雄,years和months新寫入的只也刷入到主存,但是官套,如果有JVM指令重排序酒奶,像下面這樣:

publicvoidupdate(intyears,intmonths,intdays){this.days = days;this.months = months;this.years = years;}

months和years的只在days變量修改的情況下依然會寫入到主存蚁孔,但是這時將years和days變量值刷入主存這件事發(fā)生在對months和years寫入新值之前,則對years和days的更新對其他線程來說就不可見了惋嚎。這下指令重排序就改變了程序的語義杠氢。

Java有一個應對此問題的解決方案,下面會講到另伍。

Java的volatile的Happens-Before保證

為了解決指令重排序的挑戰(zhàn)鼻百,Java的volatile關(guān)鍵字除了可見性保證之外,給出了一個“happens-before”的保證摆尝。happens-before保證如下情況:

如果讀取和寫入其他非volatile變量發(fā)生在寫入volatile變量之前(這種情況這些非volatile變量也會被刷入主存)温艇,則讀取和寫入這些變量不能被重排序為發(fā)生在寫入這個volatile變量之后(禁止指令重排序)。在寫入一個volatile變量之前的讀取和寫入非volatile變量被保證為“happen before”寫入這個volatile變量结榄。需要注意的是中贝,例如在寫入一個volatile變量之后讀寫其他變量可以被重排序到寫入這個volatile變量之前。從“之后”重排序到”之前“是允許的臼朗,但是從”之前“重排序到”之后“是禁止的邻寿。

如果讀寫其他非volatile變量發(fā)生在讀取一個volatile變量之后(這種情況這些非volatile變量也會被刷到主存),則讀寫這些變量不能被重排序為發(fā)生在讀取這個volatile變量之前视哑。需要注意的是绣否,讀取其他變量發(fā)生在讀取一個volatile變量之前能夠被重排序為發(fā)生在讀取這個volatile變量之后。從”之前“重排序到“之后”是允許的挡毅,但是從“之后”重排序到“之前”是被禁止的蒜撮。

上面的happens-before保障保證的volatile關(guān)鍵字的可見性是強制的。

volatile不總是足夠的

盡管volatile關(guān)鍵字保證了所有對一個volatile變量的讀取都是從主存中讀取跪呈,所有對volatile關(guān)鍵字的寫入都是直接到主存段磨,但是仍有其他情況使得聲明一個變量為volatile是不足夠的。

在前面解釋的情況耗绿,也就是只有Thread1寫共享變量counter苹支,聲明counter變量為volatile足以保證Thread2總是看到最新寫入的值。

實際上误阻,多線程都可以寫一個共享的volatile變量债蜜,并且仍然在主存中存儲正確的值,前提是寫入變量的新值不依賴于它之前的值究反。也就是說寻定,如果一個線程寫入一個值到共享的volatile變量不需要先去讀它的值去產(chǎn)出下一個值。

只要一個線程需要首先讀取一個volatile變量的值精耐,基于這個值生成一個新值狼速,則一個volatile關(guān)鍵字不足以保證正確的可見性。在讀取volatile變量然后寫入新值的短暫的間隙卦停,會產(chǎn)生競態(tài)條件(race condition)唐含,這時多個線程可能讀取到相同的volatile變量的值浅浮,生成這個變量的新值,當將新值寫回主存時捷枯,會覆蓋彼此的值。

多線程增加相同計數(shù)器的值就是這種情況专执,導致一個volatile聲明不足夠淮捆。下面詳細解釋這種情況。

想象如果Thread1讀取一個值為0的共享的counter變量到它的CPU緩存本股,增加1并且不將這個改變的值寫回主存攀痊。Thread2然后從主存中讀取相同的值仍為0counter變量到它的CPU緩存。Thread2也為它增加1拄显,也不寫回主存苟径。這種情況如下圖所示:

Thread1和Thread2此時實際上已經(jīng)不同步了。共享變量counter的值應該為2躬审,但是每個線程在CPU緩存中的這個變量的值都為1棘街,在主存中的值仍為0,這就亂了承边!盡管這兩個線程最終會將值寫回主存中的共享變量遭殉,這個值也是不正確的。

何時volatile是足夠的博助?

正如前面所說险污,如果兩個線程都去讀寫同一個共享變量,只對這個共享變量使用volatile關(guān)鍵字是不夠的富岳。你需要使用一個 synchronized 關(guān)鍵字去保證讀寫相同變量是原子的蛔糯。讀寫一個volatile變量不會阻塞線程的讀寫。

作為synchronized塊替代方法窖式,你可以使用 java.util.concurrent 包中的眾多原子數(shù)據(jù)類型蚁飒。比如,AtomicLong或者AtomicReference或其他的類型脖镀。

只有一個線程讀寫一個volatile變量值飒箭,其他線程只讀取變量,則這些讀線程能夠保證看到寫入這個volatile變量的最新值蜒灰,如果不聲明為volatile弦蹂,則這種情況不能保證。

volatile的性能考量

讀寫volatile變量會導致變量被讀寫到主存强窖。讀寫主存比訪問CPU緩存開銷更大凸椿。訪問volatile變量也會禁止指令重排序,而指令重排序是一個正正常的性能優(yōu)化技術(shù)翅溺。因此脑漫,你應該只在真正需要保證變量可見性的時候使用volatile變量髓抑。

需要java學習路線圖的私信筆者“java”領取哦!另外喜歡這篇文章的可以給筆者點個贊优幸,關(guān)注一下吨拍,每天都會分享Java相關(guān)文章!還有不定時的福利贈送网杆,包括整理的學習資料羹饰,面試題,源碼等~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碳却,一起剝皮案震驚了整個濱河市队秩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昼浦,老刑警劉巖馍资,帶你破解...
    沈念sama閱讀 212,599評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異关噪,居然都是意外死亡鸟蟹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評論 3 385
  • 文/潘曉璐 我一進店門色洞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來戏锹,“玉大人,你說我怎么就攤上這事火诸〗跽耄” “怎么了?”我有些...
    開封第一講書人閱讀 158,084評論 0 348
  • 文/不壞的土叔 我叫張陵置蜀,是天一觀的道長奈搜。 經(jīng)常有香客問我,道長盯荤,這世上最難降的妖魔是什么馋吗? 我笑而不...
    開封第一講書人閱讀 56,708評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮秋秤,結(jié)果婚禮上宏粤,老公的妹妹穿的比我還像新娘。我一直安慰自己灼卢,他們只是感情好绍哎,可當我...
    茶點故事閱讀 65,813評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鞋真,像睡著了一般崇堰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評論 1 291
  • 那天海诲,我揣著相機與錄音繁莹,去河邊找鬼。 笑死特幔,一個胖子當著我的面吹牛咨演,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚯斯,決...
    沈念sama閱讀 39,120評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼雪标,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了溉跃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,866評論 0 268
  • 序言:老撾萬榮一對情侶失蹤告抄,失蹤者是張志新(化名)和其女友劉穎撰茎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體打洼,經(jīng)...
    沈念sama閱讀 44,308評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡龄糊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,633評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了募疮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炫惩。...
    茶點故事閱讀 38,768評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖阿浓,靈堂內(nèi)的尸體忽然破棺而出他嚷,到底是詐尸還是另有隱情,我是刑警寧澤芭毙,帶...
    沈念sama閱讀 34,461評論 4 333
  • 正文 年R本政府宣布筋蓖,位于F島的核電站,受9級特大地震影響退敦,放射性物質(zhì)發(fā)生泄漏粘咖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,094評論 3 317
  • 文/蒙蒙 一侈百、第九天 我趴在偏房一處隱蔽的房頂上張望瓮下。 院中可真熱鬧,春花似錦钝域、人聲如沸讽坏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽震缭。三九已至,卻和暖如春战虏,著一層夾襖步出監(jiān)牢的瞬間拣宰,已是汗流浹背党涕。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巡社,地道東北人膛堤。 一個月前我還...
    沈念sama閱讀 46,571評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像晌该,于是被迫代替她去往敵國和親肥荔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,666評論 2 350

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