有時候需要采用延遲初始化來降低初始化類和建立對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。
在Java程式中,有時候可能需要推遲一些高開銷的對象初始化操作,并且隻有在使用這些對象時才進行初始化。此時,程式員可能會采用延遲初始化。
synchronized将導緻性能開銷
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
錯誤的優化
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次檢查
synchronized (DoubleCheckedLocking.class) { // 5:加鎖
if (instance == null) // 6:第二次檢查
instance = new Instance(); // 7:問題的根源出在這裡
} // 8
} // 9
return instance; // 10
} // 11
}
線上程執行到第4行,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。
前面的雙重檢查鎖定示例代碼的第7行(instance=new Singleton();)建立了一個對象。這一行代碼可以分解為如下的3行僞代碼。
memory = allocate(); // 1:配置設定對象的記憶體空間
ctorInstance(memory); // 2:初始化對象
instance = memory; // 3:設定instance指向剛配置設定的記憶體位址
上面3行僞代碼中的2和3之間,可能會被重排序,重排序不會改變單線程内的程式執行結果。
DoubleCheckedLocking
示例代碼的第7行(instance=new Singleton();)如果發生重排序,另一個并發執行的線程B就有可能在第4行判斷instance不為null。線程B接下來将通路instance所引用的對象,但此時這個對象可能還沒有被A線程初始化!
在知曉了問題發生的根源之後,我們可以想出兩個辦法來實作線程安全的延遲初始化。
1)不允許2和3重排序。
2)允許2和3重排序,但不允許其他線程“看到”這個重排序。
基于volatile的解決方案
對于前面的基于雙重檢查鎖定來實作延遲初始化的方案(指DoubleCheckedLocking示例代碼),隻需要做一點小的修改(把instance聲明為volatile型),就可以實作線程安全的延遲初始化
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance(); // instance為volatile,現在沒問題了
}
}
return instance;
}
}
基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去擷取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance ; // 這裡将導緻InstanceHolder類被初始化
}
}
這個方案的實質是:允許僞代碼中的2和3重排序,但不允許非構造線程(這裡指線程B)“看到”這個重排序。