Volatile禁止指令重排
計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令重排,一般分為以下三種:
源代碼 -> 編譯器優化的重排 -> 指令並行的重排 -> 內存系統的重排 -> 最終執行指令
單線程環境里面確保最終執行結果和代碼順序的結果一致
處理器在進行重排序時,必須要考慮指令之間的數據依賴性
多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。
指令重排 - example 1
public void mySort() {
int x = 11;
int y = 12;
x = x + 5;
y = x * x;
}
按照正常單線程環境,執行順序是 1 2 3 4
但是在多線程環境下,可能出現以下的順序:
- 2 1 3 4
- 1 3 2 4
上述的過程就可以當做是指令的重排,即內部執行順序,和我們的代碼順序不一樣
但是指令重排也是有限制的,即不會出現下面的順序
- 4 3 2 1
因為處理器在進行重排時候,必須考慮到指令之間的數據依賴性
因為步驟 4:需要依賴於 y的申明,以及x的申明,故因為存在數據依賴,無法首先執行
例子
int a,b,x,y = 0
線程1 | 線程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0; y = 0 |
因為上面的代碼,不存在數據的依賴性,因此編譯器可能對數據進行重排
線程1 | 線程2 |
---|---|
b = 1; | a = 2; |
x = a; | y = b; |
x = 2; y = 1 |
這樣造成的結果,和最開始的就不一致了,這就是導致重排后,結果和最開始的不一樣,因此為了防止這種結果出現,volatile就規定禁止指令重排,為了保證數據的一致性
指令重排 - example 2
比如下面這段代碼
public class ResortSeqDemo {
int a= 0;
boolean flag = false;
public void method01() {
a = 1;
flag = true;
}
public void method02() {
if(flag) {
a = a + 5;
System.out.println("reValue:" + a);
}
}
}
我們按照正常的順序,分別調用method01() 和 method02() 那么,最終輸出就是 a = 6
但是如果在多線程環境下,因為方法1 和 方法2,他們之間不能存在數據依賴的問題,因此原先的順序可能是
a = 1;
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
但是在經過編譯器,指令,或者內存的重排后,可能會出現這樣的情況
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
a = 1;
也就是先執行 flag = true后,另外一個線程馬上調用方法2,滿足 flag的判斷,最終讓a + 5,結果為5,這樣同樣出現了數據不一致的問題
為什么會出現這個結果:多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。
這樣就需要通過volatile來修飾,來保證線程安全性
Volatile針對指令重排做了啥
Volatile實現禁止指令重排優化,從而避免了多線程環境下程序出現亂序執行的現象
首先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個:
- 保證特定操作的順序
- 保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)
由於編譯器和處理器都能執行指令重排的優化,如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說 通過插入內存屏障禁止在內存屏障前后的指令執行重排序優化
。 內存屏障另外一個作用是刷新出各種CPU的緩存數,因此任何CPU上的線程都能讀取到這些數據的最新版本。
也就是過在Volatile的寫 和 讀的時候,加入屏障,防止出現指令重排的
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保證Load1的讀取操作在Load2及后續讀取操作之前執行 |
StoreStore | Store1;StoreStore;Store2 | 在Store2及其后的寫操作執行前,保證Store1的寫操作已刷新到主內存 |
LoadStore | Load1;LoadStore;Store2 | 在Store2及其后的寫操作執行前,保證Load1的讀操作已讀取結束 |
StoreLoad | Store1;StoreLoad;Load2 | 保證load1的寫操作已刷新到主內存之后,load2及其后的讀操作才能執行 |
線程安全獲得保證
工作內存與主內存同步延遲現象導致的可見性問題
- 可通過synchronized或volatile關鍵字解決,他們都可以使一個線程修改后的變量立即對其它線程可見
對於指令重排導致的可見性問題和有序性問題
- 可以使用volatile關鍵字解決,因為volatile關鍵字的另一個作用就是禁止重排序優化