volatile 指令重排的理解
阿新 • • 發佈:2021-07-01
指令重排
記憶體可見性只是 volatile
的其中一個語義,它還可以防止 JVM
進行指令重排優化。
舉一個虛擬碼:
int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3
一段特別簡單的程式碼,理想情況下它的執行順序是: 1>2>3
。但有可能經過 JVM 優化之後的執行順序變為了 2>1>3
。
可以發現不管 JVM 怎麼優化,前提都是保證單執行緒中最終結果不變的情況下進行的。
可能這裡還看不出有什麼問題,那看下一段虛擬碼:
private static Map<String,String> value ; private static volatile boolean flag = fasle ; //以下方法發生線上程 A 中 初始化 Map public void initMap(){ //耗時操作 value = getMapValue() ;//1 flag = true ;//2 } //發生線上程 B中 等到 Map 初始化成功進行其他操作 public void doSomeThing(){ while(!flag){ sleep() ; } //dosomething doSomeThing(value); }
這裡就能看出問題了,當 flag
沒有被 volatile
修飾時, JVM
對 1 和 2 進行重排,導致 value
都還沒有被初始化就有可能被執行緒 B 使用了。
所以加上 volatile
之後可以防止這樣的重排優化,保證業務的正確性。
指令重排的的應用
一個經典的使用場景就是雙重懶載入的單例模式了:
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { //防止指令重排 singleton = new Singleton(); } } } return singleton; } }
這裡的 volatile
關鍵字主要是為了防止指令重排。
如果不用 , singleton=newSingleton();
,這段程式碼其實是分為三步:
- 分配記憶體空間。(1)
- 初始化物件。(2)
- 將
singleton
物件指向分配的記憶體地址。(3)
加上 volatile
是為了讓以上的三步操作順序執行,反之有可能第二步在第三步之前被執行就有可能某個執行緒拿到的單例物件是還沒有初始化的,以致於報錯。