1. 程式人生 > 實用技巧 >volatile禁止指令重排

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的寫和讀的時候,加入屏障,防止出現指令重排

執行緒安全得到保證

工作記憶體與主記憶體同步延遲現象導致的可見性問題

  • 可以使用synchronized或volatile關鍵字解決,它們都可以使得一個執行緒修改後的變數立即對其他執行緒可見。
    對於指令重排導致的可見性問題和有序性問題
  • 可以利用volatile關鍵字解決,因為volatile的另一個作用就是禁止重排序優化。