Java面試官最常問的volatile關鍵字

 

在Java相關的職位面試中,不少Java面試官都喜歡考察應聘者對Java併發的瞭解程度,以volatile關鍵字爲切入點,每每會問到底,Java內存模型(JMM)和Java併發編程的一些特色都會被牽扯出來,再深刻的話還會考察JVM底層實現以及操做系統的相關知識。java

接下來讓咱們在一個假想的面試過程當中來學習一下volitile關鍵字吧。面試

1. Java併發這塊掌握的怎麼樣?來談談你對volatile關鍵字的理解吧。

參考答案:編程

個人理解是,被volatile修飾的共享變量,就會具備如下兩個特性:緩存

  1. 保證了不一樣線程對該變量操做的內存可見性。
  2. 禁止指令重排序。

2. 那你可不能夠詳細的說一下究竟什麼是內存可見性,什麼又是重排序?

參考答案:多線程

這個要是提及來可就多了,我就從Java內存模型開始提及吧。Java虛擬機規範試圖定義一個Java內存模型(JMM),以屏蔽全部類型的硬件和操做系統內存訪問差別,讓Java程序在不一樣的平臺上可以達到一致的內存訪問效果。簡單地說,因爲CPU執行指令的速度很快,可是內存訪問速度很慢,差別不是一個量級,因此搞處理器的那羣大佬們又在CPU里加了好幾層高速緩存。併發

在Java內存模型中,對上述優化進行了一波抽象。JMM規定全部的變量都在主內存中,相似於上面提到的普通內存,每一個線程又包含本身的工做內存,爲了便於理解能夠當作CPU上的寄存器或者高速緩存。所以,線程的操做都是以工做內存爲主,它們只能訪問本身的工做內存,而且在工做以前和以後,該值被同步回主內存。app

說的我本身都有點暈了,用一張圖來幫助咱們理解吧:學習

線程執行的時候,將首先從主內存讀值,再load到工做內存中的副本中,而後傳給處理器執行,執行完畢後再給工做內存中的副本賦值,隨後工做內存再把值傳回給主存,主存中的值才更新。優化

使用工做內存和主存,雖然加快了速度,但也帶來了一些問題。例如:操作系統

i = i + 1;

假設 i 的初始值爲 0 ,當只有一個線程執行它的時候,結果確定是 1 ,那麼當兩個線程執行時,獲得的結果會是 2 嗎?不必定。可能會存在這種狀況:

線程1: load i from 主存    // i = 0
        i + 1  // i = 1
線程2: load i from主存  // 由於線程1還沒將i的值寫回主存,因此i仍是0
        i +  1 //i = 1
線程1:  save i to 主存
線程2: save i to 主存

若是兩個線程遵循上面的執行過程,那麼 i 的最終值居然是 1 。若是最後的寫回生效的慢,你再讀取 i 的值,均可能會是 0 ,這就是緩存不一致的問題。

接下來就要提到您剛纔所問的問題了,JMM主要圍繞在併發過程當中如何處理併發原子性、可見性和有序性這三個特徵來創建的,經過解決這三個問題,就能夠解決緩存不一致的問題。而volatile跟可見性和有序性都有關。

3. 那你具體說說這三個特性呢?

1 . 原子性(Atomicity):

在Java中,對基本數據類型的讀取和賦值操做是原子性操做,所謂原子性操做就是指這些操做是不可中斷的,要作必定作完,要麼就沒有執行。 例如:

i = 2;
j = i;
i++;
i = i + 1;

以上四個操做, i = 2 是一個讀取操做,確定是原子性操做, j = i 你以爲是原子性操做,但事實上,能夠分爲兩個步驟,一個是讀取 i 的值,而後再把值賦給 j ,這已是兩步操做了,不能稱爲原子操做,i++ 和 i = i + 1 是等效的,讀的值,+ 1,而後寫回主存,這是三個步驟的操做了。在上面的例子中,最後一個值可能在各類狀況下,由於它不會知足原子性。

在本例中,只有一個簡單的讀取,賦值是一個原子操做,而且只能被分配給一個數字,使用變量來讀取變量的值的操做。一個例外是,在虛擬機規範中容許64位數據類型(long和double),它被劃分爲兩個32位操做,可是JDK的最新實現實現了原子操做。

JMM只實現基本的原子性,好比上面的i++操做,它必須依賴於同步和鎖定,以確保整個代碼塊的原子性。在釋放鎖以前,線程必須將I的值返回到主內存。

2 . 可見性(Visibility):

說到可見性,Java使用volatile來提供可見性。當一個變量被volatile修改時,它的變化會當即被刷新到主存,當其餘線程須要讀取變量時,它會讀取內存中的新值。普通變量不能保證。

事實上,同步和鎖定也能夠保證可見性。在釋放鎖以前,線程將把共享變量值刷回主內存,可是同步和鎖更昂貴。

3 . 有序性(Ordering)

JMM容許編譯器和處理器從新排序指令,可是指定了as-if-串行語義,也就是說,不管從新排序,程序的執行結果都不能更改。例如:

double pi = 3.14;    //A
double r = 1;        //B
double s= pi * r * r;//C

上面的語句中,能夠按照C - > B - >,結果是3.14,但它也能夠按照的順序B - > - > C,由於A和B是兩個單獨的語句,並依賴,B,C和A和B能夠從新排序,但C不能行前面的A和B。JMM確保從新排序不會影響單線程的執行,但容易出現多線程問題。例如,這樣的代碼:

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
    }
    
}

若是有兩個線程執行上面的代碼段,線程1首先執行寫,而後再乘以線程2。最後,ret的值必須是4?不必定:

如圖1和2所示,在寫方法中進行從新排序,線程1對第一個賦值爲true,而後執行到線程2,ret直接計算結果,而後再執行線程1,這一次的CaiFu值爲2,顯然是較晚的步驟。

此時要標記加上volatile關鍵字,從新排序,能夠確保程序的「順序」,也能夠基於重量級的同步和鎖定來確保,他們能夠確保在代碼執行的區域內一次性完成。

此外,JMM有一些內在的規律性,也就是說,沒有任何方法能夠保證有序,這一般稱爲發生在原則以前。<< jsr-133: Java內存模型和線程規範>>定義瞭如下事件:

  • 程序順序規則: 一個線程中的每一個操做,happens-before於該線程中的任意後續操做
  • 監視器鎖規則:對一個線程的解鎖,happens-before於隨後對這個線程的加鎖
  • volatile變量規則: 對一個volatile域的寫,happens-before於後續對這個volatile域的讀
  • 傳遞性:若是A happens-before B ,且 B happens-before C, 那麼 A happens-before C
  • start()規則: 若是線程A執行操做ThreadB_start()(啓動線程B) , 那麼A線程的ThreadB_start()happens-before 於B中的任意操做
  • join()原則: 若是A執行ThreadB.join()而且成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。
  • interrupt()原則: 對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測是否有中斷髮生
  • finalize()原則:一個對象的初始化完成先行發生於它的finalize()方法的開始

第1條程序順序規則在一個線程中,全部的操做都是有序的,但實際上只要JMM的執行結果容許從新排序,這也是發生的重點——單線程執行結果是正確的,可是也不能保證多線程。

規則2,規則監視器的規則,很是好理解。在鎖被添加以前,鎖已經被釋放,而後它才能繼續被鎖定。

第三條規則適用於討論的不穩定性。若是一個行程序編寫一個變量,另外一個線程讀取它,那麼在操做以前必須讀取寫入操做。

第四個規則正在發生。

接下來的幾行不會重複。

4. volatile關鍵字如何知足併發編程的三大特性的?

從新引入volatile變量規則是很重要的:對於一個不穩定域的寫,出現以前,而後是對這個volatile字段的讀取。本文進行再次說,若是一個變量聲明事實上是不穩定,因此當我讀了變量,最新的價值老是能夠閱讀它,這個最新的值意味着不管什麼其餘線程寫操做,該變量將當即更新到主內存,我也能夠從主內存讀取只寫值。也就是說,volatile關鍵字保證了可視性和有序度。

繼續上面的代碼示例:

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
   }
   
}

當編寫一個volatile變量時,JMM在本地內存中刷新與主內存對應的本地內存中的共享變量。

當您讀取一個volatile變量時,JMM將使線程對應的本地內存失效,而後線程將從主內存讀取共享變量。

相關文章
相關標籤/搜索