目錄
- volatile關鍵字詳解
- volatile的三個特點
- 可見性
- 概念
- 為什么需要可見性
- 禁止指令重排
- 指令重排概念
- 指令重排的問題
- 不保證原子性
- 為什么無法保證
- 具體例子
- 解決方案
- 可見性
- volatile保證可見性和解決指令重排的底層原理
- 記憶體屏障(記憶體柵欄)
- 組成
- 4種型別屏障
- 作用
- 記憶體屏障(記憶體柵欄)
- volatile的三個特點
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會把該執行緒對應的本地記憶體置為無效,執行緒接下來從主記憶體中讀取共享變數,
每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定為無效狀態, 當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中吧資料讀到處理器快取行里,
處理器使用嗅探技術保證它的內部快取,系統記憶體和其他處理器的快取在總線上保持一致
(參考下面兩位大佬的博客)
https://blog.csdn.net/jinjiniao1/article/details/100540277
https://blog.csdn.net/qq_33522040/article/details/95319946
禁止指令重排
指令重排概念
編譯器和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屏障
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/244570.html
標籤:其他
上一篇:內部類


