您好,登錄后才能下訂單哦!
在Java的面試當(dāng)中,面試官最愛問的就是volatile關(guān)鍵字相關(guān)的問題。經(jīng)過多次面試之后,你是否思考過,為什么他們那么愛問volatile關(guān)鍵字相關(guān)的問題?而對(duì)于你,如果作為面試官,是否也會(huì)考慮采用volatile關(guān)鍵字作為切入點(diǎn)呢?
為什么愛問volatile關(guān)鍵字
愛問volatile關(guān)鍵字的面試官,大多數(shù)情況下都是有一定功底的,因?yàn)関olatile作為切入點(diǎn),往底層走可以切入Java內(nèi)存模型(JMM),往并發(fā)方向走又可接切入Java并發(fā)編程,當(dāng)然,再深入追究,JVM的底層操作、字節(jié)碼的操作、單例都可以牽扯出來。
所以說懂的人提問題都是有門道的。那么,先整體來看看volatile關(guān)鍵字都設(shè)計(jì)到哪些點(diǎn):內(nèi)存可見性(JMM特性)、原子性(JMM特性)、禁止指令重排、線程并發(fā)、與synchronized的區(qū)別……再往深層次挖,可能就涉及到字節(jié)碼、JVM等。
不過值得慶幸的是,如果你已經(jīng)學(xué)習(xí)了微信公眾號(hào)“程序新視界”JVM系列的文章,上面的知識(shí)點(diǎn)已經(jīng)不是什么問題了,權(quán)當(dāng)是復(fù)習(xí)了。那么,下面就以面試官提問的形式,在不看答案的情況下,嘗試回答,看看學(xué)習(xí)效果如何。奪命連環(huán)問,開始……
面試官:說說volatile關(guān)鍵字的特性
被volatile修飾的共享變量,就具有了以下兩點(diǎn)特性:
保證了不同線程對(duì)該變量操作的內(nèi)存可見性;
禁止指令重排序;
回答的很好,點(diǎn)出了volatile關(guān)鍵字兩大特性。針對(duì)該兩大特性繼續(xù)深入。
面試官:什么是內(nèi)存可見性?能否舉例說明?
該問題涉及到Java內(nèi)存模型(JVM)和它的內(nèi)存可見性特性,這里將前面系列《Java內(nèi)存模型(JMM)詳解》和《Java內(nèi)存模型相關(guān)原則詳解》中的部分內(nèi)容整理出來回答。
先說內(nèi)存模型:Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型(JMM),來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問效果。
JMM
Java內(nèi)存模型是通過變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值,將主內(nèi)存作為傳遞媒介??膳e例說明內(nèi)存可見性的過程。
JMM
本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本,初始值都為0。線程A執(zhí)行之后把x更新為1,存放在本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把本地內(nèi)存中x=1值刷新到主內(nèi)存中,主內(nèi)存中的x值變?yōu)?。隨后,線程B到主內(nèi)存中去讀取更新后的x值,線程B的本地內(nèi)存的x值也變?yōu)榱?。
最后再說可見性:可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。
無論普通變量還是volatile變量都是如此,只不過volatile變量保證新值能夠立馬同步到主內(nèi)存,使用時(shí)也立即從主內(nèi)存刷新,保證了多線程操作時(shí)變量的可見性。而普通變量不能夠保證。
面試官:提到JMM和可見性,能說說JMM的其他特性嗎
我們知道JMM除了可見性,還有原子性和有序性。
原子性即一個(gè)操作或一系列是不可中斷的。即使是在多個(gè)線程的情況下,操作一旦開始,就不會(huì)被其他線程干擾。
比如,對(duì)于一個(gè)靜態(tài)變量int x兩條線程同時(shí)對(duì)其賦值,線程A賦值為1,而線程B賦值為2,不管線程如何運(yùn)行,最終x的值要么是1,要么是2,線程A和線程B間的操作是沒有干擾的,這就是原子性操作,不可被中斷的。
在Java內(nèi)存模型中有序性可歸納為這樣一句話:如果在本線程內(nèi)觀察,所有操作都是有序的,如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。
有序性是指對(duì)于單線程的執(zhí)行代碼,執(zhí)行是按順序依次進(jìn)行的。但在多線程環(huán)境中,則可能出現(xiàn)亂序現(xiàn)象,因?yàn)樵诰幾g過程會(huì)出現(xiàn)“指令重排”,重排后的指令與原指令的順序未必一致。
因此,上面歸納的前半句指的是線程內(nèi)保證串行語義執(zhí)行,后半句則指指“令重排現(xiàn)”象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。
面試官:你多次提到指令重排,能舉例說明嗎?
CPU和編譯器為了提升程序執(zhí)行的效率,會(huì)按照一定的規(guī)則允許進(jìn)行指令優(yōu)化。但代碼邏輯之間是存在一定的先后順序,并發(fā)執(zhí)行時(shí)按照不同的執(zhí)行邏輯會(huì)得到不同的結(jié)果。
舉個(gè)例說明多線程中可能出現(xiàn)的重排現(xiàn)象:
class ReOrderDemo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; //1
flag = true; //2
}
public void read() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在上面的代碼中,單線程執(zhí)行時(shí),read方法能夠獲得flag的值進(jìn)行判斷,獲得預(yù)期結(jié)果。但在多線程的情況下就可能出現(xiàn)不同的結(jié)果。比如,當(dāng)線程A進(jìn)行write操作時(shí),由于指令重排,write方法中的代碼執(zhí)行順序可能會(huì)變成下面這樣:
flag = true; //2
a = 1; //1
1
2
也就是說可能會(huì)先對(duì)flag賦值,然后再對(duì)a賦值。這在單線程中并不影響最終輸出的結(jié)果。
但如果與此同時(shí),B線程在調(diào)用read方法,那么就有可能出現(xiàn)flag為true但a還是0,這時(shí)進(jìn)入第4步操作的結(jié)果就為0,而不是預(yù)期的1了。
而volatile關(guān)鍵詞修飾的變量,會(huì)禁止指令重排的操作,從而在一定程度上避免了多線程中的問題。
面試官:volatile能保證原子性嗎?
volatile保證了可見性和有序性(禁止指令重排),那么能否保證原子性呢?
volatile不能保證原子性,它只是對(duì)單個(gè)volatile變量的讀/寫具有原子性,但是對(duì)于類似i 這樣的復(fù)合操作就無法保證了。
如下代碼,從直觀上來講,感覺輸出結(jié)果為10000,但實(shí)際上并不能保證,就是因?yàn)閕nc 操作屬于復(fù)合操作。
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
假設(shè)線程A,讀取了inc的值為10,然被阻塞,因未對(duì)變量進(jìn)行修改,未觸發(fā)volatile規(guī)則。線程B此時(shí)也讀取inc的值,主存里inc的值依舊為10,做自增,然后立刻寫回主存,值為11。此時(shí)線程A執(zhí)行,由于工作內(nèi)存里保存的是10,所以繼續(xù)做自增,再寫回主存,11又被寫了一遍。所以雖然兩個(gè)線程執(zhí)行了兩次increase(),結(jié)果卻只加了一次。
有人說,volatile不是會(huì)使緩存行無效的嗎?但是這里線程A讀取之后并沒有修改inc值,線程B讀取時(shí)依舊是10。又有人說,線程B將11寫回主存,不會(huì)把線程A的緩存行設(shè)為無效嗎?只有在做讀取操作時(shí),發(fā)現(xiàn)自己緩存行無效,才會(huì)去讀主存的值,而線程A的讀取操作在線程B寫入之前已經(jīng)做過了,所以這里線程A只能繼續(xù)做自增了。
針對(duì)這種情況,只能使用synchronized、Lock或并發(fā)包下的atomic的原子操作類。
面試官:剛提到synchronized,能說說它們之間的區(qū)別嗎
volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀??;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法和類級(jí)別的;
volatile僅能實(shí)現(xiàn)變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性;
volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
面試官:還能舉出其他例子說明volatile的作用嗎
可舉單例模式的實(shí)現(xiàn),典型的雙重檢查鎖定(DCL):
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) { // 1
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton(); // 2
}
}
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
這是一種懶漢的單例模式,使用時(shí)才創(chuàng)建對(duì)象,而且為了避免初始化操作的指令重排序,給instance加上了volatile。
為什么用了synchronized還要用volatile?具體來說就是synchronized雖然保證了原子性,但卻沒有保證指令重排序的正確性,會(huì)出現(xiàn)A線程執(zhí)行初始化,但可能因?yàn)闃?gòu)造函數(shù)里面的操作太多了,所以A線程的instance實(shí)例還沒有造出來,但已經(jīng)被賦值了(即代碼中2操作,先分配內(nèi)存空間后構(gòu)建對(duì)象)。http://www.daiqiyang.com
而B線程這時(shí)過來了(代碼1操作,發(fā)現(xiàn)instance不為null),錯(cuò)以為instance已經(jīng)被實(shí)例化出來,一用才發(fā)現(xiàn)instance尚未被初始化。要知道我們的線程雖然可以保證原子性,但程序可能是在多核CPU上執(zhí)行。
小結(jié)
當(dāng)然,針對(duì)volatile關(guān)鍵字還有其他方面的拓展,比如講到JMM時(shí)可拓展到JMM與Java內(nèi)存模型的區(qū)別,講到原子性時(shí)可擴(kuò)展到如何查看class字節(jié)碼,講到并發(fā)可擴(kuò)展到線程并發(fā)的方法面面。
————————————————
版權(quán)聲明:本文為CSDN博主「二師兄-公眾號(hào)-程序新視界」的原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/wo541075754/article/details/102968894
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。