volatile關鍵字詳解
volatile的三個特點
- 保證線程之間的可見性
- 禁止指令重排
- 不保證原子性
可見性
概念
可見性是多線程場景中才討論的,它表示多線程環境中,當一個線程修改了共享變量的值,其他線程能夠知道這個修改。
為什么需要可見性
緩存一致性問題:
public class Test { public static void main(String[] args) { Mythread mythread = new Mythread(); new Thread(() -> { try { //延時2s,確保進入while循環 TimeUnit.SECONDS.sleep(2); //num自增 mythread.increment(); System.out.println("Thread-" + Thread.currentThread().getName() + " current num value:" + mythread.num); } catch (Exception e) { e.printStackTrace(); } }, "test").start(); while(mythread.num == 0){ //dead } System.out.println("game over!!!"); } } class Mythread{ //不加volatile,主線程無法得知num的值發生了改變,從而陷入死循環 volatile int num = 0; public void increment(){ ++num; } }
如上述代碼,如果不加volatile,程序運行結果如下
加上volatile關鍵字后,程序運行結果如下
解決方向:
總線鎖:
一次只有一個線程能通過總線進行通信。(效率低,已棄用)
MESI緩存一致性協議,CPU總線嗅探機制(監聽機制)
有volatile修飾的共享變量在編譯器編譯后進行讀寫操作時,指令會多一個lock前綴,Lock前綴的指令在多核處理器下會引發兩件事情。
寫一個volatile變量時,JMM(java共享內存模型)會把該線程對應的本地內存中的共享變量值刷新到主內存;
當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來從主內存中讀取共享變量。
每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置為無效狀態, 當處理器對這個數據進行修改操作的時候,會重新從系統內存中吧數據讀到處理器緩存行里。
處理器使用嗅探技術保證它的內部緩存,系統內存和其他處理器的緩存在總線上保持一致
(參考下面兩位大佬的博客)
禁止指令重排
指令重排概念
編譯器和CPU在保證最終結果不變的情況下,對指令的執行順序進行重排序。
指令重排的問題
可以與雙重檢驗實現單例模式聯系起來看:
首先,一個對象的創建過程可大致分為以下三步:
- 分配內存空間
- 執行對象構造方法,初始化對象
- 引用指向實例對象在堆中的地址
但是在實際執行過程中,CPU可能會對上述步驟進行優化,進行指令重排
序1->3->2,從而導致引用指向了未初始化的對象,如果這個時候另外一個線
程引用了該未初始化的對象(只執行了1->3兩步),就會產生異常。
不保證原子性
為什么無法保證
具體例子
public class Test {
public static void main(String[] args) {
Mythread mythread = new Mythread();
for(int i = 0; i < 6666; ++i){
new Thread(() -> {
try {
mythread.increment();
} catch (Exception e) {
e.printStackTrace();
}
}, "test").start();
}
System.out.println("Thread-" + Thread.currentThread().getName() +
" current num value:" + mythread.num);
}
}
class Mythread{
volatile int num = 0;
public void increment(){
++num;
}
}
上述代碼的運行結果如下圖
可以看到,循環執行了6666次,但最后的結果為6663,說明在程序運行過程中出
現了重復的情況。
解決方案
- 使用JUC中的
Atomic
類(之后會專門寫一篇學習筆記進行闡述) - 使用synchronized關鍵字修飾(不推薦)
volatile保證可見性和解決指令重排的底層原理
內存屏障(內存柵欄)
組成
內存屏障分為兩種:Load Barrier 讀屏障 和 Store Barrier 寫屏障
4種類型屏障
種類 | 例子 | 作用 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 保證Load1讀取操作讀取完畢后再去執行Load2后續讀取操作 |
LoadStore屏障 | Load1; LoadStore; Store2 | 保證Load1讀取操作讀取完畢后再去執行Load2后續寫入操作 |
StoreStore屏障 | Store1; StoreStore; Store2 | 保證Load1的寫入對所有處理器可見后再去執行Load2后續寫入操作 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 保證Load1的寫入對所有處理器可見后再去執行Load2后續讀取操作 |
作用
-
保證特定操作的執行順序
在每個volatile修飾的全局變量讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障
-
保證某些變量的內存可見性
在每個volatile修飾的全局變量寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障