面試官最愛的volatile關鍵字

在Java相關的崗位面試中秦效,很多面試官都喜歡考察面試者對Java并發(fā)的了解程度,而以volatile關鍵字作為一個小的切入點,往往可以一問到底邓馒,把Java內存模型(JMM),Java并發(fā)編程的一些特性都牽扯出來蛾坯,深入地話還可以考察JVM底層實現(xiàn)以及操作系統(tǒng)的相關知識光酣。

下面我們以一次假想的面試過程,來深入了解下volitile關鍵字吧脉课!

面試官: Java并發(fā)這塊了解的怎么樣救军?說說你對volatile關鍵字的理解

就我理解的而言,被volatile修飾的共享變量倘零,就具有了以下兩點特性:

1 . 保證了不同線程對該變量操作的內存可見性;

2 . 禁止指令重排序唱遭。

面試官: 能不能詳細說下什么是內存可見性,什么又是重排序呢呈驶?

這個聊起來可就多了拷泽,我還是從Java內存模型說起吧。

Java虛擬機規(guī)范試圖定義一種Java內存模型(JMM),來屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異袖瞻,讓Java程序在各種平臺上都能達到一致的內存訪問效果司致。簡單來說,由于CPU執(zhí)行指令的速度是很快的虏辫,但是內存訪問的速度就慢了很多蚌吸,相差的不是一個數量級锈拨,所以搞處理器的那群大佬們又在CPU里加了好幾層高速緩存砌庄。

在Java內存模型里,對上述的優(yōu)化又進行了一波抽象奕枢。JMM規(guī)定所有變量都是存在主存中的娄昆,類似于上面提到的普通內存,每個線程又包含自己的工作內存缝彬,方便理解就可以看成CPU上的寄存器或者高速緩存萌焰。所以線程的操作都是以工作內存為主,它們只能訪問自己的工作內存谷浅,且工作前后都要把值在同步回主內存扒俯。

這么說得我自己都有些不清楚了,拿張紙畫一下:

在線程執(zhí)行時一疯,首先會從主存中read變量值撼玄,再load到工作內存中的副本中,然后再傳給處理器執(zhí)行,執(zhí)行完畢后再給工作內存中的副本賦值,隨后工作內存再把值傳回給主存童叠,主存中的值才更新涣达。

使用工作內存和主存刹碾,雖然加快的速度霸琴,但是也帶來了一些問題旬薯。比如看下面一個例子:

i=?i?+1;

假設i初值為0般又,當只有一個線程執(zhí)行它時慕蔚,結果肯定得到1丐黄,當兩個線程執(zhí)行時,會得到結果2嗎孔飒?這倒不一定了孵稽。可能存在這種情況:

如果兩個線程按照上面的執(zhí)行流程十偶,那么i最后的值居然是1了菩鲜。如果最后的寫回生效的慢,你再讀取i的值惦积,都可能是0接校,這就是緩存不一致問題。

下面就要提到你剛才問到的問題了狮崩,JMM主要就是圍繞著如何在并發(fā)過程中如何處理原子性蛛勉、可見性和有序性這3個特征來建立的,通過解決這三個問題睦柴,可以解除緩存不一致的問題诽凌。而volatile跟可見性和有序性都有關。

面試官:那你具體說說這三個特性呢坦敌?

1 . 原子性(Atomicity):

?Java中侣诵,對基本數據類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的狱窘,要做一定做完杜顺,要么就沒有執(zhí)行。

比如:

i?=?2;

j?=?i;

i++;

i?=?i?+?1蘸炸;

上面4個操作中躬络,i=2是讀取操作,必定是原子性操作搭儒,j=i你以為是原子性操作穷当,其實吧,分為兩步淹禾,一是讀取i的值馁菜,然后再賦值給j,這就是2步操作了,稱不上原子操作稀拐,i++和i = i + 1其實是等效的火邓,讀取i的值,加1,再寫回主存铲咨,那就是3步操作了躲胳。所以上面的舉例中,最后的值可能出現(xiàn)多種情況纤勒,就是因為滿足不了原子性坯苹。

這么說來,只有簡單的讀取摇天,賦值是原子操作粹湃,還只能是用數字賦值,用變量的話還多了一步讀取變量值的操作泉坐。有個例外是为鳄,虛擬機規(guī)范中允許對64位數據類型(long和double),分為2次32為的操作來處理腕让,但是最新JDK實現(xiàn)還是實現(xiàn)了原子操作的孤钦。

JMM只實現(xiàn)了基本的原子性,像上面i++那樣的操作纯丸,必須借助于synchronized和Lock來保證整塊代碼的原子性了偏形。線程在釋放鎖之前,必然會把i的值刷回到主存的觉鼻。

2 . 可見性(Visibility):

說到可見性俊扭,Java就是利用volatile來提供可見性的。

當一個變量被volatile修飾時坠陈,那么對它的修改會立刻刷新到主存萨惑,當其它線程需要讀取該變量時,會去內存中讀取新值畅姊。而普通變量則不能保證這一點咒钟。

其實通過synchronized和Lock也能夠保證可見性吹由,線程在釋放鎖之前若未,會把共享變量值都刷回主存,但是synchronized和Lock的開銷都更大倾鲫。

3 . 有序性(Ordering)

JMM是允許編譯器和處理器對指令重排序的粗合,但是規(guī)定了as-if-serial語義,即不管怎么重排序乌昔,程序的執(zhí)行結果不能改變隙疚。比如下面的程序段:

doublepi?=3.14;//A

doubler?=1;//B

doubles=?pi?*?r?*?r;//C

上面的語句,可以按照A->B->C執(zhí)行磕道,結果為3.14,但是也可以按照B->A->C的順序執(zhí)行供屉,因為A、B是兩句獨立的語句,而C則依賴于A伶丐、B悼做,所以A、B可以重排序哗魂,但是C卻不能排到A肛走、B的前面。JMM保證了重排序不會影響到單線程的執(zhí)行录别,但是在多線程中卻容易出問題朽色。

比如這樣的代碼:


假如有兩個線程執(zhí)行上述代碼段,線程1先執(zhí)行write组题,隨后線程2再執(zhí)行multiply葫男,最后ret的值一定是4嗎?結果不一定:

如圖所示崔列,write方法里的1和2做了重排序腾誉,線程1先對flag賦值為true,隨后執(zhí)行到線程2峻呕,ret直接計算出結果利职,再到線程1,這時候a才賦值為2,很明顯遲了一步瘦癌。

這時候可以為flag加上volatile關鍵字猪贪,禁止重排序,可以確保程序的“有序性”讯私,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區(qū)域里的代碼都是一次性執(zhí)行完畢的热押。

另外,JMM具備一些先天的有序性,即不需要通過任何手段就可以保證的有序性斤寇,通常稱為happens-before原則桶癣。<<JSR-133:Java Memory Model and Thread Specification>>定義了如下happens-before規(guī)則:

1.程序順序規(guī)則: 一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作

2.監(jiān)視器鎖規(guī)則:對一個線程的解鎖娘锁,happens-before于隨后對這個線程的加鎖

3.volatile變量規(guī)則: 對一個volatile域的寫牙寞,happens-before于后續(xù)對這個volatile域的讀

4.傳遞性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C

5.start()規(guī)則: 如果線程A執(zhí)行操作ThreadB_start()(啟動線程B) , 那么A線程的ThreadB_start()happens-before 于B中的任意操作

6.join()原則: 如果A執(zhí)行ThreadB.join()并且成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回莫秆。

7.interrupt()原則: 對線程interrupt()方法的調用先行發(fā)生于被中斷線程代碼檢測到中斷事件的發(fā)生间雀,可以通過Thread.interrupted()方法檢測是否有中斷發(fā)生

8.finalize()原則:一個對象的初始化完成先行發(fā)生于它的finalize()方法的開始

第1條規(guī)則程序順序規(guī)則是說在一個線程里,所有的操作都是按順序的镊屎,但是在JMM里其實只要執(zhí)行結果一樣惹挟,是允許重排序的,這邊的happens-before強調的重點也是單線程執(zhí)行結果的正確性缝驳,但是無法保證多線程也是如此连锯。

第2條規(guī)則監(jiān)視器規(guī)則其實也好理解归苍,就是在加鎖之前,確定這個鎖之前已經被釋放了运怖,才能繼續(xù)加鎖霜医。

第3條規(guī)則,就適用到所討論的volatile驳规,如果一個線程先去寫一個變量肴敛,另外一個線程再去讀,那么寫入操作一定在讀操作之前吗购。

第4條規(guī)則医男,就是happens-before的傳遞性。

后面幾條就不再一一贅述了捻勉。

面試官:volatile關鍵字如何滿足并發(fā)編程的三大特性的镀梭?

那就要重提volatile變量規(guī)則: 對一個volatile域的寫,happens-before于后續(xù)對這個volatile域的讀踱启。

這條再拎出來說报账,其實就是如果一個變量聲明成是volatile的,那么當我讀變量時埠偿,總是能讀到它的最新值透罢,這里最新值是指不管其它哪個線程對該變量做了寫操作,都會立刻被更新到主存里冠蒋,我也能從主存里讀到這個剛寫入的值羽圃。也就是說volatile關鍵字可以保證可見性以及有序性。

繼續(xù)拿上面的一段代碼舉例:


這段代碼不僅僅受到重排序的困擾抖剿,即使1朽寞、2沒有重排序。3也不會那么順利的執(zhí)行的斩郎。假設還是線程1先執(zhí)行write操作脑融,線程2再執(zhí)行multiply操作,由于線程1是在工作內存里把flag賦值為1缩宜,不一定立刻寫回主存肘迎,所以線程2執(zhí)行時,multiply再從主存讀flag值脓恕,仍然可能為false膜宋,那么括號里的語句將不會執(zhí)行。

如果改成下面這樣:

那么線程1先執(zhí)行write,線程2再執(zhí)行multiply炼幔。根據happens-before原則,這個過程會滿足以下3類規(guī)則:

1.程序順序規(guī)則:1 happens-before 2; 3 happens-before 4; (volatile限制了指令重排序史简,所以1 在2 之前執(zhí)行)

2.volatile規(guī)則:2 happens-before 3

3.傳遞性規(guī)則:1 happens-before 4

從內存語義上來看

當寫一個volatile變量時乃秀,JMM會把該線程對應的本地內存中的共享變量刷新到主內存

當讀一個volatile變量時肛著,JMM會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量跺讯。

面試官:volatile的兩點內存語義能保證可見性和有序性枢贿,但是能保證原子性嗎?

首先我回答是不能保證原子性刀脏,要是說能保證局荚,也只是對單個volatile變量的讀/寫具有原子性,但是對于類似volatile++這樣的復合操作就無能為力了愈污,比如下面的例子:


按道理來說結果是10000耀态,但是運行下很可能是個小于10000的值。有人可能會說volatile不是保證了可見性啊暂雹,一個線程對inc的修改首装,另外一個線程應該立刻看到啊杭跪!可是這里的操作inc++是個復合操作啊仙逻,包括讀取inc的值,對其自增涧尿,然后再寫回主存系奉。

假設線程A,讀取了inc的值為10姑廉,這時候被阻塞了喜最,因為沒有對變量進行修改,觸發(fā)不了volatile規(guī)則庄蹋。

線程B此時也讀讀inc的值瞬内,主存里inc的值依舊為10,做自增限书,然后立刻就被寫回主存了虫蝶,為11。

此時又輪到線程A執(zhí)行倦西,由于工作內存里保存的是10能真,所以繼續(xù)做自增,再寫回主存扰柠,11又被寫了一遍粉铐。所以雖然兩個線程執(zhí)行了兩次increase(),結果卻只加了一次卤档。

有人說蝙泼,volatile不是會使緩存行無效的嗎?但是這里線程A讀取到線程B也進行操作之前劝枣,并沒有修改inc值汤踏,所以線程B讀取的時候织鲸,還是讀的10。

又有人說溪胶,線程B將11寫回主存搂擦,不會把線程A的緩存行設為無效嗎?但是線程A的讀取操作已經做過了啊哗脖,只有在做讀取操作時瀑踢,發(fā)現(xiàn)自己緩存行無效,才會去讀主存的值才避,所以這里線程A只能繼續(xù)做自增了橱夭。

綜上所述,在這種復合操作的情景下工扎,原子性的功能是維持不了了徘钥。但是volatile在上面那種設置flag值的例子里,由于對flag的讀/寫操作都是單步的肢娘,所以還是能保證原子性的呈础。

要想保證原子性,只能借助于synchronized,Lock以及并發(fā)包下的atomic的原子操作類了橱健,即對基本數據類型的 自增(加1操作)而钞,自減(減1操作)、以及加法操作(加一個數)拘荡,減法操作(減一個數)進行了封裝臼节,保證這些操作是原子性操作。

面試官:說的還可以珊皿,那你知道volatile底層的實現(xiàn)機制网缝?

如果把加入volatile關鍵字的代碼和未加入volatile關鍵字的代碼都生成匯編代碼,會發(fā)現(xiàn)加入volatile關鍵字的代碼會多出一個lock前綴指令蟋定。

lock前綴指令實際相當于一個內存屏障粉臊,內存屏障提供了以下功能:

1 . 重排序時不能把后面的指令重排序到內存屏障之前的位置

2 . 使得本CPU的Cache寫入內存

3 . 寫入動作也會引起別的CPU或者別的內核無效化其Cache,相當于讓新寫入的值對別的線程可見驶兜。

面試官: 你在哪里會使用到volatile扼仲,舉兩個例子呢?

1. 狀態(tài)量標記抄淑,就如上面對flag的標記屠凶,我重新提一下:

這種對變量的讀寫操作,標記為volatile可以保證修改對線程立刻可見肆资。比synchronized,Lock有一定的效率提升矗愧。

2. 單例模式的實現(xiàn),典型的雙重檢查鎖定(DCL)

這是一種懶漢的單例模式迅耘,使用時才創(chuàng)建對象贱枣,而且為了避免初始化操作的指令重排序监署,給instance加上了volatile颤专。

面試官: 來給我們說說幾種單例模式的寫法吧纽哥,還有上面這種用法,你再詳細說說呢栖秕?

好吧春塌,這又是一個話題了,volatile的問題終于問完了簇捍。只壳。∈钏埽看看你掌握了沒~?

擴展閱讀

簡單的單例模式其實也不簡單

深入理解synchronized關鍵字

Java并發(fā)編程之volatile關鍵字解析

關于Java鎖機制面試官會怎么問

金三銀四銅五鐵六吼句,面試得做好這個準備

作者:卡巴拉的樹

來源:https://juejin.im/post/5a2b53b7f265da432a7b821c

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市事格,隨后出現(xiàn)的幾起案子惕艳,更是在濱河造成了極大的恐慌,老刑警劉巖驹愚,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件远搪,死亡現(xiàn)場離奇詭異,居然都是意外死亡逢捺,警方通過查閱死者的電腦和手機谁鳍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劫瞳,“玉大人倘潜,你說我怎么就攤上這事≈居冢” “怎么了涮因?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恨憎。 經常有香客問我蕊退,道長,這世上最難降的妖魔是什么憔恳? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任瓤荔,我火速辦了婚禮,結果婚禮上钥组,老公的妹妹穿的比我還像新娘输硝。我一直安慰自己,他們只是感情好程梦,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布点把。 她就那樣靜靜地躺著橘荠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪郎逃。 梳的紋絲不亂的頭發(fā)上哥童,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音褒翰,去河邊找鬼贮懈。 笑死,一個胖子當著我的面吹牛优训,可吹牛的內容都是我干的朵你。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼揣非,長吁一口氣:“原來是場噩夢啊……” “哼忌傻!你這毒婦竟也來了搁嗓?” 一聲冷哼從身側響起芯勘,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎安疗,沒想到半個月后荐类,有當地人在樹林里發(fā)現(xiàn)了一具尸體玉罐,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扭屁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片料滥。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡罪治,死狀恐怖,靈堂內的尸體忽然破棺而出浴井,到底是詐尸還是另有隱情磺浙,我是刑警寧澤撕氧,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布锦溪,位于F島的核電站刻诊,受9級特大地震影響复局,放射性物質發(fā)生泄漏亿昏。R本人自食惡果不足惜龙优,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望平道。 院中可真熱鬧,春花似錦冀墨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茁影,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浩螺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工仍侥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人患蹂。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓传于,卻偏偏與公主長得像平挑,于是被迫代替她去往敵國和親找都。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353