Java面試官最愛問的volatile關鍵字
在Java的面試當中,面試官最愛問的就是volatile關鍵字相關的問題。經過多次面試之後,你是否思考過,為什麼他們那麼愛問volatile關鍵字相關的問題?而對於你,如果作為面試官,是否也會考慮採用volatile關鍵字作為切入點呢?
為什麼愛問volatile關鍵字
愛問volatile關鍵字的面試官,大多數情況下都是有一定功底的,因為volatile作為切入點,往底層走可以切入Java記憶體模型(JMM),往併發方向走又可接切入Java併發程式設計,當然,再深入追究,JVM的底層操作、位元組碼的操作、單例都可以牽扯出來。
所以說懂的人提問題都是有門道的。那麼,先整體來看看volatile關鍵字都設計到哪些點:記憶體可見性(JMM特性)、原子性(JMM特性)、禁止指令重排、執行緒併發、與synchronized的區別……再往深層次挖,可能就涉及到位元組碼、JVM等。
不過值得慶幸的是,如果你已經學習了微信公眾號“程式新視界”JVM系列的文章,上面的知識點已經不是什麼問題了,權當是複習了。那麼,下面就以面試官提問的形式,在不看答案的情況下,嘗試回答,看看學習效果如何。奪命連環問,開始……
面試官:說說volatile關鍵字的特性
被volatile修飾的共享變數,就具有了以下兩點特性:
- 保證了不同執行緒對該變數操作的記憶體可見性;
- 禁止指令重排序;
回答的很好,點出了volatile關鍵字兩大特性。針對該兩大特性繼續深入。
面試官:什麼是記憶體可見性?能否舉例說明?
該問題涉及到Java記憶體模型(JVM)和它的記憶體可見性特性,這裡將前面系列《Java記憶體模型(JMM)詳解》和《Java記憶體模型相關原則詳解》中的部分內容整理出來回答。
先說記憶體模型:Java虛擬機器規範試圖定義一種Java記憶體模型(JMM),來遮蔽掉各種硬體和作業系統的記憶體訪問差異,讓Java程式在各種平臺上都能達到一致的記憶體訪問效果。
Java記憶體模型是通過變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值,將主記憶體作為傳遞媒介。可舉例說明記憶體可見性的過程。
本地記憶體A和B有主記憶體中共享變數x的副本,初始值都為0。執行緒A執行之後把x更新為1,存放在本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把本地記憶體中x=1值重新整理到主記憶體中,主記憶體中的x值變為1。隨後,執行緒B到主記憶體中去讀取更新後的x值,執行緒B的本地記憶體的x值也變為了1。
最後再說可見性:可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。
無論普通變數還是volatile變數都是如此,只不過volatile變數保證新值能夠立馬同步到主記憶體,使用時也立即從主記憶體重新整理,保證了多執行緒操作時變數的可見性。而普通變數不能夠保證。
面試官:提到JMM和可見性,能說說JMM的其他特性嗎
我們知道JMM除了可見性,還有原子性和有序性。
原子性即一個操作或一系列是不可中斷的。即使是在多個執行緒的情況下,操作一旦開始,就不會被其他執行緒干擾。
比如,對於一個靜態變數int x兩條執行緒同時對其賦值,執行緒A賦值為1,而執行緒B賦值為2,不管執行緒如何執行,最終x的值要麼是1,要麼是2,執行緒A和執行緒B間的操作是沒有干擾的,這就是原子性操作,不可被中斷的。
在Java記憶體模型中有序性可歸納為這樣一句話:如果在本執行緒內觀察,所有操作都是有序的,如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。
有序性是指對於單執行緒的執行程式碼,執行是按順序依次進行的。但在多執行緒環境中,則可能出現亂序現象,因為在編譯過程會出現“指令重排”,重排後的指令與原指令的順序未必一致。
因此,上面歸納的前半句指的是執行緒內保證序列語義執行,後半句則指指“令重排現”象和“工作記憶體與主記憶體同步延遲”現象。
面試官:你多次提到指令重排,能舉例說明嗎?
CPU和編譯器為了提升程式執行的效率,會按照一定的規則允許進行指令優化。但程式碼邏輯之間是存在一定的先後順序,併發執行時按照不同的執行邏輯會得到不同的結果。
舉個例說明多執行緒中可能出現的重排現象:
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
……
}
}
}
在上面的程式碼中,單執行緒執行時,read方法能夠獲得flag的值進行判斷,獲得預期結果。但在多執行緒的情況下就可能出現不同的結果。比如,當執行緒A進行write操作時,由於指令重排,write方法中的程式碼執行順序可能會變成下面這樣:
flag = true; //2
a = 1; //1
也就是說可能會先對flag賦值,然後再對a賦值。這在單執行緒中並不影響最終輸出的結果。
但如果與此同時,B執行緒在呼叫read方法,那麼就有可能出現flag為true但a還是0,這時進入第4步操作的結果就為0,而不是預期的1了。
而volatile關鍵詞修飾的變數,會禁止指令重排的操作,從而在一定程度上避免了多執行緒中的問題。
面試官:volatile能保證原子性嗎?
volatile保證了可見性和有序性(禁止指令重排),那麼能否保證原子性呢?
volatile不能保證原子性,它只是對單個volatile變數的讀/寫具有原子性,但是對於類似i 這樣的複合操作就無法保證了。
如下程式碼,從直觀上來講,感覺輸出結果為10000,但實際上並不能保證,就是因為inc 操作屬於複合操作。
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) //保證前面的執行緒都執行完
Thread.yield();
System.out.println(test.inc);
}
假設執行緒A,讀取了inc的值為10,然被阻塞,因未對變數進行修改,未觸發volatile規則。執行緒B此時也讀取inc的值,主存裡inc的值依舊為10,做自增,然後立刻寫回主存,值為11。此時執行緒A執行,由於工作記憶體裡儲存的是10,所以繼續做自增,再寫回主存,11又被寫了一遍。所以雖然兩個執行緒執行了兩次increase(),結果卻只加了一次。
有人說,volatile不是會使快取行無效的嗎?但是這裡執行緒A讀取之後並沒有修改inc值,執行緒B讀取時依舊是10。又有人說,執行緒B將11寫回主存,不會把執行緒A的快取行設為無效嗎?只有在做讀取操作時,發現自己快取行無效,才會去讀主存的值,而執行緒A的讀取操作線上程B寫入之前已經做過了,所以這裡執行緒A只能繼續做自增了。
針對這種情況,只能使用synchronized、Lock或併發包下的atomic的原子操作類。
面試官:剛提到synchronized,能說說它們之間的區別嗎
- volatile本質是在告訴JVM當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。
- volatile僅能使用在變數級別;synchronized則可以使用在變數、方法和類級別的;
- volatile僅能實現變數的修改可見性,不能保證原子性;而synchronized則可以保證變數的修改可見性和原子性;
- volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
- volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化。
面試官:還能舉出其他例子說明volatile的作用嗎
可舉單例模式的實現,典型的雙重檢查鎖定(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;
}
}
這是一種懶漢的單例模式,使用時才建立物件,而且為了避免初始化操作的指令重排序,給instance加上了volatile。
為什麼用了synchronized還要用volatile?具體來說就是synchronized雖然保證了原子性,但卻沒有保證指令重排序的正確性,會出現A執行緒執行初始化,但可能因為建構函式裡面的操作太多了,所以A執行緒的instance例項還沒有造出來,但已經被賦值了(即程式碼中2操作,先分配記憶體空間後構建物件)。
而B執行緒這時過來了(程式碼1操作,發現instance不為null),錯以為instance已經被例項化出來,一用才發現instance尚未被初始化。要知道我們的執行緒雖然可以保證原子性,但程式可能是在多核CPU上執行。
小結
當然,針對volatile關鍵字還有其他方面的擴充,比如講到JMM時可擴充到JMM與Java記憶體模型的區別,講到原子性時可擴充套件到如何檢視class位元組碼,講到併發可擴充套件到執行緒併發的方法面面。
其實,不僅面試如此,在學習知識時也可以參考這種面試思維,多問幾個為什麼。將一個點,通過為什麼擴充成一個知識網。
原文連結:《Java面試官最愛問的volatile關鍵字》
《面試官》系列文章:
- 《JVM之記憶體結構詳解》
- 《面試官,不要再問我“Java GC垃圾回收機制”了》
- 《面試官,Java8 JVM記憶體結構變了,永久代到元空間》
- 《面試官,不要再問我“Java 垃圾收集器”了》
- 《Java虛擬機器類載入器及雙親委派機制》
- 《Java記憶體模型(JMM)詳解》
- 《Java記憶體模型相關原則詳解》
- 《Java面試官最愛問的volatile關鍵字》
相關文章
- 面試官最愛的volatile關鍵字面試
- Java 面試官最喜歡問的關鍵字 volatileJava面試
- Java面試題集錦(1):volatile關鍵字Java面試題
- Java面試題:請談談Java中的volatile關鍵字?Java面試題
- Java關鍵字volatile的理解Java
- Java volatile關鍵字作用Java
- Java volatile關鍵字解析Java
- volatile關鍵字解析~高階java必問Java
- Java執行緒面試題(03) Java中的volatile如何工作? Java中的volatile關鍵字示例Java執行緒面試題
- 深入瞭解 Java 的 volatile 關鍵字Java
- Volatile關鍵字
- java併發之volatile關鍵字Java
- Java併發—— 關鍵字volatile解析Java
- 深入理解Java中的volatile關鍵字Java
- Java記憶體模型——volatile關鍵字Java記憶體模型
- volatile關鍵字解析
- volatile關鍵字的作用、原理
- Java併發程式設計volatile關鍵字Java程式設計
- Java多執行緒(二)volatile關鍵字Java執行緒
- java併發程式設計——volatile關鍵字Java程式設計
- java記憶體模型及volatile關鍵字Java記憶體模型
- java併發程式設計:volatile關鍵字Java程式設計
- 深入彙編指令理解Java關鍵字volatileJava
- java多執行緒4:volatile關鍵字Java執行緒
- Java記憶體模型與volatile關鍵字Java記憶體模型
- 關於java volatile關鍵字,以後別再面試中說不清楚了Java面試
- 快速理解 volatile 關鍵字
- 深入解析volatile關鍵字
- 從根源上解析 Java volatile 關鍵字的實現Java
- Java併發程式設計:volatile關鍵字解析Java程式設計
- 一個具體的例子學習Java volatile關鍵字Java
- 兩張圖理解volatile關鍵字
- 面試官愛問的equals與hashCode面試
- 面試 HTTP ,99% 的面試官都愛問這些問題面試HTTP
- C語言中volatile關鍵字的作用C語言
- volatile 關鍵字的工作機制
- Java之併發程式設計:volatile關鍵字解析Java程式設計
- Java多執行緒學習(三)volatile關鍵字Java執行緒