在Java的面試當中,面試官最愛問的就是volatile關鍵字相關的問題。通過屢次面試以後,你是否思考過,爲何他們那麼愛問volatile關鍵字相關的問題?而對於你,若是做爲面試官,是否也會考慮採用volatile關鍵字做爲切入點呢?c++
愛問volatile關鍵字的面試官,大多數狀況下都是有必定功底的,由於volatile做爲切入點,往底層走能夠切入Java內存模型(JMM),往併發方向走又可接切入Java併發編程,固然,再深刻追究,JVM的底層操做、字節碼的操做、單例均可以牽扯出來。面試
因此說懂的人提問題都是有門道的。那麼,先總體來看看volatile關鍵字都設計到哪些點:內存可見性(JMM特性)、原子性(JMM特性)、禁止指令重排、線程併發、與synchronized的區別……再往深層次挖,可能就涉及到字節碼、JVM等。編程
不過值得慶幸的是,若是你已經學習了微信公衆號「程序新視界」JVM系列的文章,上面的知識點已經不是什麼問題了,權當是複習了。那麼,下面就以面試官提問的形式,在不看答案的狀況下,嘗試回答,看看學習效果如何。奪命連環問,開始……segmentfault
被volatile修飾的共享變量,就具備瞭如下兩點特性:緩存
回答的很好,點出了volatile關鍵字兩大特性。針對該兩大特性繼續深刻。微信
該問題涉及到Java內存模型(JVM)和它的內存可見性特性,這裏將前面系列《Java內存模型(JMM)詳解》和《Java內存模型相關原則詳解》中的部份內容整理出來回答。多線程
先說內存模型:Java虛擬機規範試圖定義一種Java內存模型(JMM),來屏蔽掉各類硬件和操做系統的內存訪問差別,讓Java程序在各類平臺上都能達到一致的內存訪問效果。併發
Java內存模型是經過變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值,將主內存做爲傳遞媒介。可舉例說明內存可見性的過程。jvm
本地內存A和B有主內存中共享變量x的副本,初始值都爲0。線程A執行以後把x更新爲1,存放在本地內存A中。當線程A和線程B須要通訊時,線程A首先會把本地內存中x=1值刷新到主內存中,主內存中的x值變爲1。隨後,線程B到主內存中去讀取更新後的x值,線程B的本地內存的x值也變爲了1。函數
最後再說可見性:可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。
不管普通變量仍是volatile變量都是如此,只不過volatile變量保證新值可以立馬同步到主內存,使用時也當即從主內存刷新,保證了多線程操做時變量的可見性。而普通變量不可以保證。
咱們知道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變量的讀/寫具備原子性,可是對於相似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的原子操做類。
可舉單例模式的實現,典型的雙重檢查鎖定(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關鍵字》
《面試官》系列文章:
<center>程序新視界:精彩和成長都不容錯過</center>