在Java相關(guān)的崗位面試中夫晌,很多面試官都喜歡考察面試者對(duì)Java并發(fā)的了解程度,而以volatile關(guān)鍵字作為一個(gè)小的切入點(diǎn)昧诱,往往可以一問(wèn)到底晓淀,把Java內(nèi)存模型(JMM),Java并發(fā)編程的一些特性都牽扯出來(lái)鳄哭,深入地話(huà)還可以考察JVM底層實(shí)現(xiàn)以及操作系統(tǒng)的相關(guān)知識(shí)要糊。
下面我們以一次假想的面試過(guò)程,來(lái)深入了解下volitile關(guān)鍵字吧妆丘!
面試官: Java并發(fā)這塊了解的怎么樣锄俄?說(shuō)說(shuō)你對(duì)volatile關(guān)鍵字的理解
就我理解的而言,被volatile修飾的共享變量勺拣,就具有了以下兩點(diǎn)特性:
1 . 保證了不同線程對(duì)該變量操作的內(nèi)存可見(jiàn)性;
2 . 禁止指令重排序
面試官: 能不能詳細(xì)說(shuō)下什么是內(nèi)存可見(jiàn)性奶赠,什么又是重排序呢?
這個(gè)聊起來(lái)可就多了药有,我還是從Java內(nèi)存模型說(shuō)起吧毅戈。
Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型(JMM),來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問(wèn)效果愤惰。簡(jiǎn)單來(lái)說(shuō)苇经,由于CPU執(zhí)行指令的速度是很快的,但是內(nèi)存訪問(wèn)的速度就慢了很多宦言,相差的不是一個(gè)數(shù)量級(jí)扇单,所以搞處理器的那群大佬們又在CPU里加了好幾層高速緩存。
在Java內(nèi)存模型里奠旺,對(duì)上述的優(yōu)化又進(jìn)行了一波抽象蜘澜。JMM規(guī)定所有變量都是存在主存中的,類(lèi)似于上面提到的普通內(nèi)存响疚,每個(gè)線程又包含自己的工作內(nèi)存鄙信,方便理解就可以看成CPU上的寄存器或者高速緩存。所以線程的操作都是以工作內(nèi)存為主忿晕,它們只能訪問(wèn)自己的工作內(nèi)存装诡,且工作前后都要把值在同步回主內(nèi)存。
這么說(shuō)得我自己都有些不清楚了,拿張紙畫(huà)一下:
在線程執(zhí)行時(shí)慎王,首先會(huì)從主存中read變量值蚓土,再load到工作內(nèi)存中的副本中宏侍,然后再傳給處理器執(zhí)行赖淤,執(zhí)行完畢后再給工作內(nèi)存中的副本賦值,隨后工作內(nèi)存再把值傳回給主存谅河,主存中的值才更新咱旱。
使用工作內(nèi)存和主存,雖然加快的速度绷耍,但是也帶來(lái)了一些問(wèn)題吐限。比如看下面一個(gè)例子:
i = i + 1;
假設(shè)i初值為0,當(dāng)只有一個(gè)線程執(zhí)行它時(shí)褂始,結(jié)果肯定得到1诸典,當(dāng)兩個(gè)線程執(zhí)行時(shí),會(huì)得到結(jié)果2嗎崎苗?這倒不一定了狐粱。可能存在這種情況:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
線程1: load i from 主存 // i = 0
i + 1 // i = 1
線程2: load i from主存 // 因?yàn)榫€程1還沒(méi)將i的值寫(xiě)回主存胆数,所以i還是0
i + 1 //i = 1
線程1: save i to 主存
線程2: save i to 主存
</pre>
如果兩個(gè)線程按照上面的執(zhí)行流程肌蜻,那么i最后的值居然是1了。如果最后的寫(xiě)回生效的慢必尼,你再讀取i的值蒋搜,都可能是0,這就是緩存不一致問(wèn)題判莉。
下面就要提到你剛才問(wèn)到的問(wèn)題了豆挽,JMM主要就是圍繞著如何在并發(fā)過(guò)程中如何處理原子性、可見(jiàn)性和有序性這3個(gè)特征來(lái)建立的券盅,通過(guò)解決這三個(gè)問(wèn)題帮哈,可以解除緩存不一致的問(wèn)題。而volatile跟可見(jiàn)性和有序性都有關(guān)渗饮。
面試官:那你具體說(shuō)說(shuō)這三個(gè)特性呢但汞?
1 . 原子性(Atomicity): Java中,對(duì)基本數(shù)據(jù)類(lèi)型的讀取和賦值操作是原子性操作互站,所謂原子性操作就是指這些操作是不可中斷的私蕾,要做一定做完,要么就沒(méi)有執(zhí)行胡桃。
比如:
i = 2;j = i;i++;i = i + 1踩叭;
上面4個(gè)操作中,i=2
是讀取操作,必定是原子性操作容贝,j=i
你以為是原子性操作自脯,其實(shí)吧,分為兩步斤富,一是讀取i的值膏潮,然后再賦值給j,這就是2步操作了,稱(chēng)不上原子操作满力,i++
和i = i + 1
其實(shí)是等效的焕参,讀取i的值,加1油额,再寫(xiě)回主存叠纷,那就是3步操作了。所以上面的舉例中潦嘶,最后的值可能出現(xiàn)多種情況涩嚣,就是因?yàn)闈M(mǎn)足不了原子性。
這么說(shuō)來(lái)掂僵,只有簡(jiǎn)單的讀取航厚,賦值是原子操作,還只能是用數(shù)字賦值看峻,用變量的話(huà)還多了一步讀取變量值的操作阶淘。有個(gè)例外是,虛擬機(jī)規(guī)范中允許對(duì)64位數(shù)據(jù)類(lèi)型(long和double)互妓,分為2次32為的操作來(lái)處理溪窒,但是最新JDK實(shí)現(xiàn)還是實(shí)現(xiàn)了原子操作的。
JMM只實(shí)現(xiàn)了基本的原子性冯勉,像上面i++
那樣的操作澈蚌,必須借助于synchronized
和Lock
來(lái)保證整塊代碼的原子性了。線程在釋放鎖之前灼狰,必然會(huì)把i
的值刷回到主存的宛瞄。
2 . 可見(jiàn)性(Visibility):
說(shuō)到可見(jiàn)性,Java就是利用volatile來(lái)提供可見(jiàn)性的交胚。
當(dāng)一個(gè)變量被volatile修飾時(shí)份汗,那么對(duì)它的修改會(huì)立刻刷新到主存,當(dāng)其它線程需要讀取該變量時(shí)蝴簇,會(huì)去內(nèi)存中讀取新值杯活。而普通變量則不能保證這一點(diǎn)。
其實(shí)通過(guò)synchronized和Lock也能夠保證可見(jiàn)性熬词,線程在釋放鎖之前旁钧,會(huì)把共享變量值都刷回主存吸重,但是synchronized和Lock的開(kāi)銷(xiāo)都更大。
3 . 有序性(Ordering)
JMM是允許編譯器和處理器對(duì)指令重排序的歪今,但是規(guī)定了as-if-serial語(yǔ)義嚎幸,即不管怎么重排序,程序的執(zhí)行結(jié)果不能改變寄猩。比如下面的程序段:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
double pi = 3.14; //A
double r = 1; //B
double s= pi * r * r;//C
</pre>
上面的語(yǔ)句嫉晶,可以按照A->B->C
執(zhí)行,結(jié)果為3.14,但是也可以按照B->A->C
的順序執(zhí)行焦影,因?yàn)锳车遂、B是兩句獨(dú)立的語(yǔ)句封断,而C則依賴(lài)于A斯辰、B,所以A坡疼、B可以重排序彬呻,但是C卻不能排到A、B的前面柄瑰。JMM保證了重排序不會(huì)影響到單線程的執(zhí)行闸氮,但是在多線程中卻容易出問(wèn)題。
比如這樣的代碼:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
</pre>
假如有兩個(gè)線程執(zhí)行上述代碼段教沾,線程1先執(zhí)行write蒲跨,隨后線程2再執(zhí)行multiply,最后ret的值一定是4嗎授翻?結(jié)果不一定:
如圖所示或悲,write方法里的1和2做了重排序,線程1先對(duì)flag賦值為true堪唐,隨后執(zhí)行到線程2巡语,ret直接計(jì)算出結(jié)果,再到線程1淮菠,這時(shí)候a才賦值為2,很明顯遲了一步男公。
這時(shí)候可以為flag加上volatile關(guān)鍵字,禁止重排序合陵,可以確保程序的“有序性”枢赔,也可以上重量級(jí)的synchronized和Lock來(lái)保證有序性,它們能保證那一塊區(qū)域里的代碼都是一次性執(zhí)行完畢的。
另外拥知,JMM具備一些先天的有序性,即不需要通過(guò)任何手段就可以保證的有序性踏拜,通常稱(chēng)為happens-before原則。<<JSR-133:Java Memory Model and Thread Specification>>
定義了如下happens-before規(guī)則:
程序順序規(guī)則: 一個(gè)線程中的每個(gè)操作举庶,happens-before于該線程中的任意后續(xù)操作
監(jiān)視器鎖規(guī)則:對(duì)一個(gè)線程的解鎖执隧,happens-before于隨后對(duì)這個(gè)線程的加鎖
volatile變量規(guī)則: 對(duì)一個(gè)volatile域的寫(xiě),happens-before于后續(xù)對(duì)這個(gè)volatile域的讀
傳遞性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C
start()規(guī)則: 如果線程A執(zhí)行操作
ThreadB_start()
(啟動(dòng)線程B) , 那么A線程的ThreadB_start()
happens-before 于B中的任意操作join()原則: 如果A執(zhí)行
ThreadB.join()
并且成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()
操作成功返回镀琉。interrupt()原則: 對(duì)線程
interrupt()
方法的調(diào)用先行發(fā)生于被中斷線程代碼檢測(cè)到中斷事件的發(fā)生峦嗤,可以通過(guò)Thread.interrupted()
方法檢測(cè)是否有中斷發(fā)生finalize()原則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的
finalize()
方法的開(kāi)始
第1條規(guī)則程序順序規(guī)則是說(shuō)在一個(gè)線程里,所有的操作都是按順序的屋摔,但是在JMM里其實(shí)只要執(zhí)行結(jié)果一樣烁设,是允許重排序的,這邊的happens-before強(qiáng)調(diào)的重點(diǎn)也是單線程執(zhí)行結(jié)果的正確性钓试,但是無(wú)法保證多線程也是如此装黑。
第2條規(guī)則監(jiān)視器規(guī)則其實(shí)也好理解,就是在加鎖之前弓熏,確定這個(gè)鎖之前已經(jīng)被釋放了恋谭,才能繼續(xù)加鎖。
第3條規(guī)則挽鞠,就適用到所討論的volatile疚颊,如果一個(gè)線程先去寫(xiě)一個(gè)變量,另外一個(gè)線程再去讀信认,那么寫(xiě)入操作一定在讀操作之前材义。
第4條規(guī)則,就是happens-before的傳遞性嫁赏。
后面幾條就不再一一贅述了其掂。
面試官:volatile關(guān)鍵字如何滿(mǎn)足并發(fā)編程的三大特性的?
那就要重提volatile變量規(guī)則: 對(duì)一個(gè)volatile域的寫(xiě)潦蝇,happens-before于后續(xù)對(duì)這個(gè)volatile域的讀款熬。
這條再拎出來(lái)說(shuō),其實(shí)就是如果一個(gè)變量聲明成是volatile的护蝶,那么當(dāng)我讀變量時(shí)华烟,總是能讀到它的最新值,這里最新值是指不管其它哪個(gè)線程對(duì)該變量做了寫(xiě)操作持灰,都會(huì)立刻被更新到主存里盔夜,我也能從主存里讀到這個(gè)剛寫(xiě)入的值。也就是說(shuō)volatile關(guān)鍵字可以保證可見(jiàn)性以及有序性堤魁。
繼續(xù)拿上面的一段代碼舉例:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
</pre>
這段代碼不僅僅受到重排序的困擾喂链,即使1、2沒(méi)有重排序妥泉。3也不會(huì)那么順利的執(zhí)行的椭微。假設(shè)還是線程1先執(zhí)行write
操作,線程2再執(zhí)行multiply
操作盲链,由于線程1是在工作內(nèi)存里把flag賦值為1蝇率,不一定立刻寫(xiě)回主存迟杂,所以線程2執(zhí)行時(shí),multiply
再?gòu)闹鞔孀xflag值本慕,仍然可能為false排拷,那么括號(hào)里的語(yǔ)句將不會(huì)執(zhí)行。
如果改成下面這樣:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
int a = 0;
volatile bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
</pre>
那么線程1先執(zhí)行write
,線程2再執(zhí)行multiply
锅尘。根據(jù)happens-before原則监氢,這個(gè)過(guò)程會(huì)滿(mǎn)足以下3類(lèi)規(guī)則:
程序順序規(guī)則:1 happens-before 2; 3 happens-before 4; (volatile限制了指令重排序,所以1 在2 之前執(zhí)行)
volatile規(guī)則:2 happens-before 3
傳遞性規(guī)則:1 happens-before 4
從內(nèi)存語(yǔ)義上來(lái)看
當(dāng)寫(xiě)一個(gè)volatile變量時(shí)藤违,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存
當(dāng)讀一個(gè)volatile變量時(shí)浪腐,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,線程接下來(lái)將從主內(nèi)存中讀取共享變量顿乒。
面試官:volatile的兩點(diǎn)內(nèi)存語(yǔ)義能保證可見(jiàn)性和有序性议街,但是能保證原子性嗎?
首先我回答是不能保證原子性淆游,要是說(shuō)能保證傍睹,也只是對(duì)單個(gè)volatile變量的讀/寫(xiě)具有原子性,但是對(duì)于類(lèi)似volatile++這樣的復(fù)合操作就無(wú)能為力了犹菱,比如下面的例子:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
</pre>
按道理來(lái)說(shuō)結(jié)果是10000,但是運(yùn)行下很可能是個(gè)小于10000的值吮炕。有人可能會(huì)說(shuō)volatile不是保證了可見(jiàn)性啊腊脱,一個(gè)線程對(duì)inc的修改,另外一個(gè)線程應(yīng)該立刻看到傲住陕凹!可是這里的操作inc++是個(gè)復(fù)合操作啊,包括讀取inc的值鳄炉,對(duì)其自增杜耙,然后再寫(xiě)回主存。
假設(shè)線程A拂盯,讀取了inc的值為10佑女,這時(shí)候被阻塞了,因?yàn)闆](méi)有對(duì)變量進(jìn)行修改谈竿,觸發(fā)不了volatile規(guī)則团驱。
線程B此時(shí)也讀讀inc的值,主存里inc的值依舊為10空凸,做自增嚎花,然后立刻就被寫(xiě)回主存了,為11呀洲。
此時(shí)又輪到線程A執(zhí)行紊选,由于工作內(nèi)存里保存的是10啼止,所以繼續(xù)做自增,再寫(xiě)回主存兵罢,11又被寫(xiě)了一遍族壳。所以雖然兩個(gè)線程執(zhí)行了兩次increase(),結(jié)果卻只加了一次趣些。
有人說(shuō)仿荆,volatile不是會(huì)使緩存行無(wú)效的嗎?但是這里線程A讀取到線程B也進(jìn)行操作之前坏平,并沒(méi)有修改inc值拢操,所以線程B讀取的時(shí)候,還是讀的10舶替。
又有人說(shuō)令境,線程B將11寫(xiě)回主存,不會(huì)把線程A的緩存行設(shè)為無(wú)效嗎顾瞪?但是線程A的讀取操作已經(jīng)做過(guò)了啊舔庶,只有在做讀取操作時(shí),發(fā)現(xiàn)自己緩存行無(wú)效陈醒,才會(huì)去讀主存的值惕橙,所以這里線程A只能繼續(xù)做自增了。
綜上所述钉跷,在這種復(fù)合操作的情景下弥鹦,原子性的功能是維持不了了。但是volatile在上面那種設(shè)置flag值的例子里爷辙,由于對(duì)flag的讀/寫(xiě)操作都是單步的彬坏,所以還是能保證原子性的。
要想保證原子性膝晾,只能借助于synchronized,Lock以及并發(fā)包下的atomic的原子操作類(lèi)了烛恤,即對(duì)基本數(shù)據(jù)類(lèi)型的 自增(加1操作)雹熬,自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝干发,保證這些操作是原子性操作裳扯。
面試官:說(shuō)的還可以铜秆,那你知道volatile底層的實(shí)現(xiàn)機(jī)制誓琼?
如果把加入volatile關(guān)鍵字的代碼和未加入volatile關(guān)鍵字的代碼都生成匯編代碼,會(huì)發(fā)現(xiàn)加入volatile關(guān)鍵字的代碼會(huì)多出一個(gè)lock前綴指令巍扛。
lock前綴指令實(shí)際相當(dāng)于一個(gè)內(nèi)存屏障领跛,內(nèi)存屏障提供了以下功能:
1 . 重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
2 . 使得本CPU的Cache寫(xiě)入內(nèi)存
3 . 寫(xiě)入動(dòng)作也會(huì)引起別的CPU或者別的內(nèi)核無(wú)效化其Cache,相當(dāng)于讓新寫(xiě)入的值對(duì)別的線程可見(jiàn)撤奸。
面試官: 你在哪里會(huì)使用到volatile吠昭,舉兩個(gè)例子呢喊括?
1. 狀態(tài)量標(biāo)記,就如上面對(duì)flag的標(biāo)記矢棚,我重新提一下:
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
int a = 0;
volatile bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
</pre>
這種對(duì)變量的讀寫(xiě)操作郑什,標(biāo)記為volatile可以保證修改對(duì)線程立刻可見(jiàn)。比synchronized,Lock有一定的效率提升蒲肋。
2.單例模式的實(shí)現(xiàn)蘑拯,典型的雙重檢查鎖定(DCL)
<pre class="" style="margin: 0px; padding: 2px; max-width: 100%; box-sizing: inherit; overflow-wrap: normal; -webkit-tap-highlight-color: transparent; overflow-x: auto; background-color: rgb(63, 63, 63); color: rgb(220, 220, 220); border-radius: 3px; line-height: 1.4; font-size: 16px;">
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
</pre>
這是一種懶漢的單例模式,使用時(shí)才創(chuàng)建對(duì)象兜粘,而且為了避免初始化操作的指令重排序申窘,給instance加上了volatile。