volatile關鍵字詳解
阿新 • • 發佈:2021-01-04
[TOC]
# volatile關鍵字詳解
## **volatile的三個特點**
1. 保證執行緒之間的可見性
2. 禁止指令重排
3. 不保證原子性
### **可見性**
#### **概念**
> **可見性**是多執行緒場景中才討論的,它表示多執行緒環境中,當一個執行緒修改了共享變數的值,其他執行緒能夠知道這個修改。
#### **為什麼需要可見性**
> 快取一致性問題:
>
> ```java
> 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](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210104193610527-1088999671.png)
>
> 加上volatile關鍵字後,程式執行結果如下
>
> ![加上volatile](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210104193640230-1523388574.png)
>
>
>
> 解決方向:
>
> - 匯流排鎖:
>
> 一次只有一個執行緒能通過匯流排進行通訊。(效率低,已棄用)
>
> - 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在保證最終結果不變的情況下,對指令的執行順序進行重排序。
#### **指令重排的問題**
> 可以與**雙重檢驗實現單例模式**聯絡起來看:
>
> 首先,一個物件的建立過程可大致分為以下三步:
>
> 1. 分配記憶體空間
> 2. 執行物件構造方法,初始化物件
> 3. 引用指向例項物件在堆中的地址
>
> 但是在實際執行過程中,CPU可能會對上述步驟進行優化,進行指令重排
>
> 序1->3->2,從而導致引用指向了未初始化的物件,如果這個時候**另外一個線**
>
> **程引用了該未初始化的物件(只執行了1->3兩步)**,就會產生異常。
### **不保證原子性**
#### **為什麼無法保證**
#### **具體例子**
```java
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;
}
}
```
上述程式碼的執行結果如下圖
![](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210104194839367-1534500039.png)
可以看到,迴圈執行了6666次,但最後的結果為6663,說明在程式執行過程中出
現了重複的情況。
#### **解決方案**
1. 使用JUC中的`Atomic`類(之後會專門寫一篇學習筆記進行闡述)
2. 使用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後續讀取操作 |
#### **作用**
1. **保證特定操作的執行順序**
> 在每個volatile修飾的全域性變數**讀操作前**插入**LoadLoad**屏障,在**讀操作後**插入**LoadStore**屏障
2. **保證某些變數的記憶體可見性**
> 在每個volatile修飾的全域性變數**寫操作前**插入**StoreStore**屏障,在**寫操作後**插入**StoreLoad*