面試時,面試官經常會通過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代碼,我們來看下這個問題的答案。
在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的操作,能立即回寫到主內存,所以其它線程也能立即可見,所以能支持並發。
當大家從volatile關鍵字引申到ConcurrentHashmap底層源碼后,面試官就會認識你很資深。我記得當初,我去面試一家比較大的互聯網公司,就這樣說了一通,然后就直接通過這輪技術面試了(不過還有后繼部門經理的技術面試)。
請大家關注我的公眾號:一起進步,一起掙錢,在本公眾號里,會有很多精彩的面試文章。