面試時通過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底層源碼後,面試官就會認識你很資深。我記得當初,我去面試一家比較大的互聯網公司,就這樣說了一通,然後就直接通過這輪技術面試了(不過還有後繼部門經理的技術面試)。

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章