寫在開頭
在之前的幾篇博文中,我們都提到了 volatile
關鍵字,這個單詞中文釋義為:不穩定的,易揮發的,在Java中代表變數修飾符,用來修飾會被不同執行緒訪問和修改的變數,對於方法,程式碼塊,方法引數,區域性變數以及例項常量,類常量多不能進行修飾。
自JDK1.5之後,官網對volatile進行了語義增強,這讓它在Java多執行緒領域越發重要!因此,我們今天就抽一晚上時間,來學一學這個關鍵字,首先,我們從標題入手,思考這樣的一個問題:
volatile如何保證可見性,具體如何實現的?
帶著疑問,我們繼續往下閱讀!
volatile如何保證可見性
volatile保證了不同執行緒對共享變數進行操作時的可見性,即一個執行緒修改了共享變數的值,共享變數修改後的值對其他執行緒立即可見。
我們先透過之前寫的一個小案例來感受一下什麼是可見性問題:
【程式碼示例1】
public class Test {
//是否停止 變數
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
//啟動執行緒 1,當 stop 為 true,結束迴圈
new Thread(() -> {
System.out.println("執行緒 1 正在執行...");
while (!stop) ;
System.out.println("執行緒 1 終止");
}).start();
//休眠 1 秒
Thread.sleep(1000);
//啟動執行緒 2, 設定 stop = true
new Thread(() -> {
System.out.println("執行緒 2 正在執行...");
stop = true;
System.out.println("設定 stop 變數為 true.");
}).start();
}
}
輸出:
執行緒 1 正在執行...
執行緒 2 正在執行...
設定 stop 變數為 true.
原因:
我們會發現,執行緒1執行起來後,休眠1秒,啟動執行緒2,可即便執行緒2把stop設定為true了,執行緒1仍然沒有停止,這個就是因為 CPU 快取導致的可見性導致的問題。執行緒 2 設定 stop 變數為 true,執行緒 1 在 CPU 1上執行,讀取的 CPU 1 快取中的 stop 變數仍然為 false,執行緒 1 一直在迴圈執行。
那這個問題怎麼解決呢?很好解決!我們排volatile上場可以秒搞定,只需要給stop變數加上volatile修飾符即可!
【程式碼示例2】
//給stop變數增加volatile修飾符
private static volatile boolean stop = false;
輸出:
執行緒 1 正在執行...
執行緒 2 正在執行...
設定 stop 變數為 true.
執行緒 1 終止
從結果中看,執行緒1成功的讀取到了執行緒而設定為true的stop變數值,解決了可見性問題。那volatile到底是什麼讓變數在多個執行緒之間保持可見性的呢?請看下圖!
如果我們將變數宣告為 volatile ,這就指示 JVM,這個變數是共享且不穩定的,每次使用它都到主存中進行讀取,具體實現可總結為5步。
- 1️⃣在生成最低成彙編指令時,對volatile修飾的共享變數寫操作增加Lock字首指令,Lock 字首的指令會引起 CPU 快取寫回記憶體;
- 2️⃣CPU 的快取回寫到記憶體會導致其他 CPU 快取了該記憶體地址的資料無效;
- 3️⃣volatile 變數透過快取一致性協議保證每個執行緒獲得最新值;
- 4️⃣快取一致性協議保證每個 CPU 透過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是修改;
- 5️⃣當 CPU 發現自己快取行對應的記憶體地址被修改,會將當前 CPU 的快取行設定成無效狀態,重新從記憶體中把資料讀到 CPU 快取。
總結
其實volatile關鍵字不僅僅能解決可見性問題,還可以透過禁止編譯器、CPU 指令重排序和部分 happens-before 規則,解決有序性問題,我們放在下一篇聊。
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!