- 指令重排基礎概念
- 在現代處理器和編譯器為了提高程序執行效率,會對指令進行優化,其中一種優化方式就是指令重排序。在單線程環境下,指令重排序不會影響最終執行結果,因為處理器和編譯器會保證重排序后的執行結果與按照代碼順序執行的結果一致。但在多線程環境下,指令重排序可能會導致程序出現意外的行為。
- 例如,處理器可能會將一些不依賴于其他指令結果的指令提前執行,以充分利用處理器資源。編譯器也可能對代碼進行優化,改變指令的順序。
volatile
禁止指令重排原理volatile
關鍵字具有禁止指令重排序的語義。Java內存模型(JMM)規定,對volatile
變量的寫操作,先行發生于后續對這個volatile
變量的讀操作。這意味著,在volatile
變量的寫操作之前的所有操作,都必須在volatile
變量的寫操作之前完成;而volatile
變量的讀操作,必須在其之后的所有操作之前完成。這種規則確保了volatile
變量相關的操作順序與代碼順序一致,從而避免了指令重排序帶來的問題。
- 代碼示例及執行順序分析
- 示例1:單例模式中的指令重排問題與
volatile
作用
- 示例1:單例模式中的指令重排問題與
public class Singleton {// 未使用volatile時可能會出現指令重排問題private static Singleton instance; private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查// 這里可能發生指令重排instance = new Singleton(); }}}return instance;}
}
在上述代碼中,instance = new Singleton();
這行代碼實際上包含了三個步驟:
-
- 分配內存空間給
Singleton
對象。
- 分配內存空間給
-
- 初始化
Singleton
對象。
- 初始化
-
- 將
instance
指向分配的內存空間。
- 將
在沒有 volatile
修飾的情況下,編譯器和處理器可能會對這三個步驟進行重排序,比如先執行步驟1和3,然后再執行步驟2。假設線程A執行 getInstance()
方法,在步驟1和3執行后,但步驟2還未執行時,線程B進入 getInstance()
方法,此時 instance
已經非空(因為已經指向了分配的內存空間),線程B會直接返回 instance
,但此時 instance
還未初始化完成,這就會導致程序出錯。
使用 volatile
修飾 instance
后:
public class Singleton {// 使用volatile防止指令重排private static volatile Singleton instance; private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton(); }}}return instance;}
}
volatile
關鍵字保證了這三個步驟不會被重排序,一定是按照1 -> 2 -> 3的順序執行,從而確保了 instance
在被其他線程獲取時已經完全初始化。
- 示例2:簡單變量操作中的指令重排與
volatile
public class VolatileReorderingExample {private static int a = 0;private static volatile boolean flag = false;public static void main(String[] args) {Thread thread1 = new Thread(() -> {a = 1; // 語句1flag = true; // 語句2});Thread thread2 = new Thread(() -> {while (!flag) {// 等待flag變為true}System.out.println(a); // 語句3});thread1.start();thread2.start();}
}
在上述代碼中,如果 flag
沒有被聲明為 volatile
,由于指令重排,語句1和語句2的執行順序可能會被改變,即先執行 flag = true;
然后再執行 a = 1;
。這樣當線程2執行到 System.out.println(a);
時,a
可能還沒有被賦值為1,輸出結果可能為0。
而當 flag
被聲明為 volatile
后,JMM保證了語句1一定在語句2之前執行,并且語句1的結果對線程2可見。所以當線程2執行 System.out.println(a);
時,a
一定已經被賦值為1,輸出結果為1。
總結來說,volatile
通過JMM的規則,限制了編譯器和處理器對 volatile
變量相關指令的重排序,確保了多線程環境下程序的執行順序符合預期,避免了因指令重排導致的錯誤。