1. 程式人生 > >從單例模式挖到記憶體模型(二)----指令重排序

從單例模式挖到記憶體模型(二)----指令重排序

首先是一個雙檢鎖寫的單例模式的例子:

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,記憶體系統的重排序:處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。