首先是一個雙檢鎖寫的單例模式的例子:
public class Single{
private volatile static Single single;
private Single(){}
public static Single getInstance(){
if(single==null){
synchronized (Single.class) {
if(single==null){
single=new Single();
}
}
}
return single;
}
}
下面分析一下指令重排序(也有名字叫亂序執行,無序寫入)給這個單例模式帶來的問題:
要分析上面例子中存在的問題,就要從instance = new Singleton()這句開始,對java來說,建立新的對象并不是一個原子操作,這個過程分成了3步:
1,給 instance 配置設定記憶體
2,調用 Singleton 的構造函數來初始化成員變量
3,将instance對象指向配置設定的記憶體空間(執行完這步 instance 就為非 null 了)
關鍵:
1,在JVM的即時編譯器中,存在一個設定,叫做指令重排序。
2,在上面的例子中,2操作依賴1操作,但3操作并不依賴2操作,也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是1-2-3 也可能是1-3-2。如果是後者,則在3執行完畢,2未執行之前,被線程二搶占了,這時instance已經是非 null 了(但卻沒有初始化),是以線程二會直接傳回 instance,然後使用,然後順理成章地報錯。
3,JDK1.5以後,因為記憶體模型的優化,上面的例子不會再因為指令重排序而出現問題。
關于指令重排序的說明:
1,JVM為了使得處理器内部的運算單元能充分利用,使效率最大化,處理器可能會對輸入代碼進行指令重排序的優化,處理器會在計算之後将亂序執行的結果進行重組,保證該結果與順序執行的結果是一樣的,但并不保證程式中各個語句計算的先後順序與輸入的代碼順序一緻(這種保證一緻的原則叫做as-if-serial)。
2,在多線程的情況下,指令的重排序可能會影響計算的結果。
3,如果java認為兩個操作有資料依賴性,則不會重排序。
重排序有三種,在某一次編譯的過程中,這三種重排序的情形有可能都出現:
1,編譯器優化重排序:編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。
2,指令級并行的重排序:如果不存l在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
3,記憶體系統的重排序:處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。