java 面試之 volatile 關(guān)鍵字

一次面試經(jīng)歷:
面試官:請(qǐng)講一下 volatile湿酸。
我:volatile 是 java 虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制辜御,當(dāng)變量定義為 volatile 后伍派,可以保證此變量對(duì)多線程的可見(jiàn)性江耀。多個(gè)線程可以讀到內(nèi)存中最新的值。
面試管:volatile 底層具體怎么實(shí)現(xiàn)的诉植? 怎么保證的可見(jiàn)性祥国?
我:。晾腔。舌稀。。
面試官:volatile 怎么保證多線程可以讀到最新的值灼擂?
我:壁查。。剔应。睡腿。

面試結(jié)果可想而知了,面試官隨便問(wèn)了問(wèn)就送我出門(mén)了领斥。
所以打算好好研究一下嫉到。

先來(lái)介紹幾個(gè)知識(shí)點(diǎn):

Java 內(nèi)存模型

主內(nèi)存與工作內(nèi)存

Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量?jī)?chǔ)存到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)月洛。注意何恶,這里的變量是指實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素嚼黔,不包括局部變量與方法參數(shù)细层,因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享唬涧,就不存在競(jìng)爭(zhēng)問(wèn)題疫赎。

Java 內(nèi)存模型有主內(nèi)存和工作內(nèi)存,工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝碎节,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行捧搞,而不能直接讀寫(xiě)主內(nèi)存中的變量,不同線程之間無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存的變量狮荔,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成胎撇。

注意:

當(dāng)一個(gè)變量被volatile修飾后,JMM 會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存殖氏。表示著線程工作內(nèi)存無(wú)效晚树,當(dāng)一個(gè)線程修改共享變量后他會(huì)立即被更新到主內(nèi)存中,當(dāng)其他線程讀取共享變量時(shí)雅采,它會(huì)直接從主內(nèi)存中讀取爵憎。

指令重排序

在執(zhí)行程序時(shí)為了提高性能慨亲,編譯器和處理器通常會(huì)對(duì)指令做重排序:
1.編譯器重排序。編譯器在不改變單線程程序語(yǔ)義的前提下宝鼓,可以重新安排語(yǔ)句的執(zhí)行順序刑棵;
2.處理器重排序。如果不存在數(shù)據(jù)依賴性愚铡,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序铐望;

指令重排序?qū)尉€程沒(méi)有什么影響,他不會(huì)影響程序的運(yùn)行結(jié)果茂附,但是會(huì)影響多線程的正確性,既然指令重排序會(huì)影響到多線程執(zhí)行的正確性督弓,那么我們就需要禁止重排序营曼。那么JVM是如何禁止重排序的呢?這個(gè)問(wèn)題稍后回答愚隧,我們先看另一個(gè)原則happens-before蒂阱,happen-before原則保證了程序的“有序性”,它規(guī)定如果兩個(gè)操作的執(zhí)行順序無(wú)法從happens-before原則中推到出來(lái)狂塘,那么他們就不能保證有序性录煤,可以隨意進(jìn)行重排序。其定義如下:

1.同一個(gè)線程中的荞胡,前面的操作 happen-before 后續(xù)的操作妈踊。(即單線程內(nèi)按代碼順序執(zhí)行。但是泪漂,在不影響在單線程環(huán)境執(zhí)行結(jié)果的前提下廊营,編譯器和處理器可以進(jìn)行重排序,這是合法的萝勤。換句話說(shuō)露筒,這一是規(guī)則無(wú)法保證編譯重排和指令重排)。

2.監(jiān)視器上的解鎖操作 happen-before 其后續(xù)的加鎖操作敌卓。(Synchronized 規(guī)則)慎式。
3.對(duì)volatile變量的寫(xiě)操作 happen-before 后續(xù)的讀操作。(volatile 規(guī)則)趟径。
4.線程的start() 方法 happen-before 該線程所有的后續(xù)操作瘪吏。(線程啟動(dòng)規(guī)則)。
5.線程所有的操作 happen-before 其他線程在該線程上調(diào)用 join 返回成功后的操作舵抹。
6.如果 a happen-before b肪虎,b happen-before c,則a happen-before c(傳遞性)惧蛹。

我們著重看第三點(diǎn)volatile規(guī)則:對(duì)volatile變量的寫(xiě)操作 happen-before 后續(xù)的讀操作扇救。為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義刑枝,JMM會(huì)重排序,

注意

觀察加入volatile關(guān)鍵字和沒(méi)有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn)迅腔,加入volatile關(guān)鍵字時(shí)装畅,會(huì)多出一個(gè)lock前綴指令。lock前綴指令其實(shí)就相當(dāng)于一個(gè)內(nèi)存屏障沧烈。內(nèi)存屏障是一組處理指令掠兄,用來(lái)實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。volatile的底層就是通過(guò)內(nèi)存屏障來(lái)實(shí)現(xiàn)的锌雀。

內(nèi)存屏障

為了保證內(nèi)存可見(jiàn)性蚂夕,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來(lái)禁止特定類(lèi)型的處理器重排序。JMM把內(nèi)存屏障指令分為4類(lèi)腋逆,如下表:


屏幕快照 2019-01-07 21.51.37.png

volatile內(nèi)存語(yǔ)義實(shí)現(xiàn)

為了實(shí)現(xiàn) volatile 的內(nèi)存語(yǔ)義婿牍,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型的處理器重排序惩歉。下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:

  • 在每個(gè) volatile 寫(xiě)操作的前面插入一個(gè) StoreStore 屏障(禁止前面的寫(xiě)與volatile寫(xiě)重排序)等脂。
  • 在每個(gè) volatile 寫(xiě)操作的后面插入一個(gè) StoreLoad 屏障(禁止volatile寫(xiě)與后面可能有的讀和寫(xiě)重排序)。
  • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障(禁止volatile讀與后面的讀操作重排序)撑蚌。
  • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadStore 屏障(禁止volatile讀與后面的寫(xiě)操作重排序)上遥。

其中重點(diǎn)說(shuō)下StoreLaod屏障,它是確闭浚可見(jiàn)性的關(guān)鍵粉楚,因?yàn)樗鼤?huì)將屏障之前的寫(xiě)緩沖區(qū)中的數(shù)據(jù)全部刷新到主內(nèi)存中。上述內(nèi)存屏障插入策略非常保守第煮,但它可以保證在任意處理平臺(tái)解幼,任意的程序中都能得到正確的volatile語(yǔ)義。下面是保守策略(為什么說(shuō)保守呢包警,因?yàn)橛行┰趯?shí)際的場(chǎng)景是可省略的)下撵摆,volatile 寫(xiě)操作 插入內(nèi)存屏障后生成的指令序列示意圖:

屏幕快照 2019-01-07 21.46.22.png

其中StoreStore屏障可以保證在volatile寫(xiě)之前,其前面的所有普通寫(xiě)操作對(duì)任意處理器可見(jiàn)(把它刷新到主內(nèi)存)害晦。另外volatile寫(xiě)后面有StoreLoad屏障特铝,此屏障的作用是避免volatile寫(xiě)與后面可能有的讀或?qū)懖僮鬟M(jìn)行重排序。因?yàn)榫幾g器常常無(wú)法準(zhǔn)確判斷在一個(gè)volatile寫(xiě)的后面是否需要插入一個(gè)StoreLoad屏障(比如壹瘟,一個(gè)volatile寫(xiě)之后方法立即return)為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義鲫剿,JMM采取了保守策略:在每個(gè)volatile寫(xiě)的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫(xiě)-讀內(nèi)存語(yǔ)義的常見(jiàn)模式是:一個(gè)寫(xiě)線程寫(xiě)volatile變量稻轨,多個(gè)度線程讀同一個(gè)volatile變量灵莲。當(dāng)讀線程的數(shù)量大大超過(guò)寫(xiě)線程時(shí),選擇在volatile寫(xiě)之后插入StoreLoad屏障將帶來(lái)可觀的執(zhí)行效率的提升殴俱。從這里也可看出JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性政冻,然后再去追求效率(其實(shí)我們工作中編碼也是一樣)枚抵。

下面是在保守策略下,volatile讀插入內(nèi)存屏障后生產(chǎn)的指令序列示意圖:


屏幕快照 2019-01-07 21.46.33.png

上述volatile寫(xiě)和volatile讀的內(nèi)存屏障插入策略非常保守明场。在實(shí)際執(zhí)行時(shí)汽摹,只要不改變volatile寫(xiě)-讀的內(nèi)存語(yǔ)義,編譯器可以根據(jù)具體情況忽略不必要的屏障苦锨。在JMM基礎(chǔ)中就有提到過(guò)各個(gè)處理器對(duì)各個(gè)屏障的支持度逼泣,其中x86處理器僅會(huì)對(duì)寫(xiě)-讀操作做重排序。

原子性

原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷舟舒,要么就都不執(zhí)行拉庶。

一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問(wèn)題:

比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元秃励,往賬戶B加上1000元砍的。

試想一下,如果這2個(gè)操作不具備原子性莺治,會(huì)造成什么樣的后果。假如從賬戶A減去1000元之后帚稠,操作突然中止谣旁。然后又從B取出了500元,取出500元之后滋早,再執(zhí)行 往賬戶B加上1000元 的操作榄审。這樣就會(huì)導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒(méi)有收到這個(gè)轉(zhuǎn)過(guò)來(lái)的1000元杆麸。

所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問(wèn)題搁进。

同樣地反映到并發(fā)編程中會(huì)出現(xiàn)什么結(jié)果呢?

舉個(gè)最簡(jiǎn)單的例子昔头,大家想一下假如為一個(gè)32位的變量賦值過(guò)程不具備原子性的話饼问,會(huì)發(fā)生什么后果?

i = 9;

假若一個(gè)線程執(zhí)行到這個(gè)語(yǔ)句時(shí)揭斧,我暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過(guò)程:為低16位賦值莱革,為高16位賦值。

那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫(xiě)入之后讹开,突然被中斷盅视,而此時(shí)又有一個(gè)線程去讀取i的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)旦万。

對(duì)于被 volatile 修飾的變量闹击,對(duì)任意(包括64位long類(lèi)型和double類(lèi)型)單個(gè)volatile變量的讀/寫(xiě)具有原子性,記著是對(duì)單個(gè)volatile變量的讀或?qū)懖啪哂性有猿伤遥硗馊魏螐?fù)合操作都不能保證原子性赏半,如a++贺归,a = a+1, a = b。特別注意a = b這類(lèi)除破,它實(shí)際上包含2個(gè)操作牧氮,它先要去讀取b的值,再將b的值寫(xiě)入工作內(nèi)存瑰枫,雖然讀取b的值以及將b的值寫(xiě)入工作內(nèi)存這2個(gè)操作都是原子性操作踱葛,但是合起來(lái)就不是原子性操作了。

想要理解透volatile特性有一個(gè)很好的方法光坝,就是把對(duì)volatile變量的單個(gè)讀/寫(xiě)尸诽,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫(xiě)操作做了同步。

總結(jié)

至此應(yīng)該對(duì) volatile 有比較好的了解了盯另,至少面試應(yīng)該問(wèn)題不大了性含,其實(shí)就是上面幾個(gè)關(guān)鍵點(diǎn),可見(jiàn)性鸳惯,重排序商蕴,內(nèi)存屏障,原子性芝发。把這些底層都研究透了绪商,面試官根本難不倒你。

參考資料:
深入理解 Java 虛擬機(jī)
Java 并發(fā)編程的藝術(shù)
Java 多線程編程核心技術(shù)
https://www.cnblogs.com/yuanfy008/p/9335168.html
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/chenssy/p/6379280.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辅鲸,一起剝皮案震驚了整個(gè)濱河市格郁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌独悴,老刑警劉巖例书,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異刻炒,居然都是意外死亡决采,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)坟奥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)织狐,“玉大人,你說(shuō)我怎么就攤上這事筏勒∫破龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵管行,是天一觀的道長(zhǎng)厨埋。 經(jīng)常有香客問(wèn)我,道長(zhǎng)捐顷,這世上最難降的妖魔是什么荡陷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任雨效,我火速辦了婚禮,結(jié)果婚禮上废赞,老公的妹妹穿的比我還像新娘徽龟。我一直安慰自己,他們只是感情好唉地,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布据悔。 她就那樣靜靜地躺著,像睡著了一般耘沼。 火紅的嫁衣襯著肌膚如雪极颓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天群嗤,我揣著相機(jī)與錄音菠隆,去河邊找鬼。 笑死狂秘,一個(gè)胖子當(dāng)著我的面吹牛骇径,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播者春,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼既峡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了碧查?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤校仑,失蹤者是張志新(化名)和其女友劉穎忠售,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體迄沫,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稻扬,尸身上長(zhǎng)有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
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望徽惋。 院中可真熱鬧案淋,春花似錦、人聲如沸险绘。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)隆圆。三九已至漱挚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渺氧,已是汗流浹背旨涝。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侣背,地道東北人白华。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像贩耐,于是被迫代替她去往敵國(guó)和親弧腥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

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