面試時通過volatile關鍵字,全面展示執行緒記憶體模型的能力

    面試時,面試官經常會通過volatile關鍵字來考核候選人在多執行緒方面的能力,一旦被問題此類問題,大家可以通過如下的步驟全面這方面的能力。

    1 首先通過記憶體模型說明volatile關鍵字的作用

    先說明,用volatile修飾的變數,能直接修改記憶體內容,修改後的變數對其他執行緒是可見的。然後展開說明如下的內容。

    多執行緒並發操作同一資源時,可能會出現最終結果和預期不同的情況,剛才我們也已經通過執行緒安全和不安全相關的案例,直觀地看到了這一情況,這裡我們將通過執行緒的記憶體結構來詳細分析下造成「最終結果不一致」的原因。

    如果某個執行緒要操作data變數,該執行緒會先把data變數裝載到執行緒內部的記憶體中做個副本,之後執行緒就不再和在主記憶體中的data變數有任何關係,而是會操作副本變數的值,操作完成後,再把這個副本回寫到主記憶體(也就是堆記憶體)中,這個過程如下圖所示。

    假設data的初始值是0,有100個執行緒並發地對它進行加1操作,預期的運行結果是100。但在實際的操作過程中,假設A執行緒和B執行緒並發地data,其中A讀到的值是0,B讀到的是1。當B在它的執行緒內部記憶體中完成加1操作(data變成2),會把data回寫到主記憶體里,這時主記憶體里的data也是2。

    但之後,A執行緒也完成了加1操作(此時A內部執行緒中的data副本是1),在之後的回寫過程中,會把主記憶體中的data變數從2設置成1,這樣就造成數據不一致的問題了。

    但是,如果data變數被volatile變數修飾,那麼A執行緒修改好的data變數,無需等到「」回寫「」階段,能直接寫回到主記憶體里,這就能導致該變數對其它執行緒「立即可見」。

2 同時說明,volatile不能解決數據不一致的問題

如果某個變數之前加了volatile,執行緒在每次使用該變數時,都會從主記憶體中讀取該變數最新的值,而且,某執行緒一旦修改了該變數,這個修改會立即回寫到主記憶體里。

既然是在操作前會從主記憶體中讀取變數最新的值,而且每次修改後都會立即回寫到主記憶體,這樣的話是否能解決多執行緒中數據不一致的問題呢?通過下面的VolilateDemo.java程式碼,我們來看下這個問題的答案。

1	public class VolilateDemo extends Thread {
2		//啟動1000個執行緒,對這個被volatile修飾的變數進行加1操作
3	    public static volatile int cnt = 0;
4		public static void add() {
5			// 延遲1毫秒,增加多執行緒並發搶佔的概率
6			try { Thread.sleep(1);}
7	        catch (InterruptedException e) {	}
8			cnt++;//加操作
9		}
10		public static void main(String[] args) {
11			// 同時啟動1000個執行緒,去進行加操作
12			for (int i = 0; i < 1000; i++) {
13				new Thread(new Runnable() {
14					public void run() 
15	                 {VolilateDemo.add();	}
16				}).start();
17			}
18			System.out.println("Result is " + VolilateDemo.cnt);
19		}
20	}

    在main函數的第12行里,通過for循環啟動1000個執行緒。從第13到16行里,我們通過了Runnable類定義了執行緒的動作,每個執行緒啟動後,會調用第15行的add方法對用volatile修飾的cnt變數進行加1操作。

    多次運行的結果可能不一樣,但在大多數情況下,最終cnt的值會小於1000,也就是說,用volatile修飾的變數不能保證數據一致性,換句話說,volatile不能當鎖來用,因為它不能保證主記憶體的變數在同一時間段里只被一個執行緒操作。

3 然後說下volatile的作用

     那麼volatile有什麼用呢?被volatile修飾的變數每次在使用時,不是從各執行緒的內部記憶體中拿,而是從主記憶體中拿。這樣就能避免「創建副本」到「把副本回寫到主記憶體中」等的操作,從而能提升效率。

    但請注意,如果我們在多執行緒環境下,針對某個變數有讀和寫的操作,那麼別把它修飾成volatile,因為為了解決數據不一致的問題,我們會給該變數加鎖,這樣該變數在一個時間段里只會有一個執行緒進行操作,這樣就無法發揮出volatile的優勢了。

    請記住這個結論,如果某個變數在多執行緒環境下只有讀或者是只有寫的操作,建議把它設置成volatile,這樣能提升多執行緒並發時的效率。

4 如果可以,再擴展到ConcurrentHashMap的底層程式碼

    說好上述內容以後,其實大家已經可以能充分展示記憶體方面的技能了,不過大家還可以多說一句:我還看過ConcurrentHashMap的底層源碼,其中用到了volatile關鍵字。

    ConcurrentHashMap是支援並發的HashMap,說白了就當多個執行緒同時讀寫ConcurrentHashMap對象時,不會有問題。

    該對象存儲鍵值對的Node對象定義如下,其中表示值的val變數被volatile修飾,也就是說,A執行緒對該ConcurrentHashMap的操作,能立即回寫到主記憶體,所以其它執行緒也能立即可見,所以能支援並發。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    //可以看到這些都用了volatile修飾
    volatile V val;
    volatile Node<K,V> next;

    省略其它程式碼 
}

    當大家從volatile關鍵字引申到ConcurrentHashmap底層源碼後,面試官就會認識你很資深。我記得當初,我去面試一家比較大的互聯網公司,就這樣說了一通,然後就直接通過這輪技術面試了(不過還有後繼部門經理的技術面試)。

請大家關注我的公眾號:一起進步,一起掙錢,在本公眾號里,會有很多精彩的面試文章。

 

Tags: