有時候需要採用延遲初始化來降低初始化類和創建對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。
在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)“看到”這個重排序。