概述
在Java多線程程序中,有時候需要採用延遲初始化來降低初始化類和創建對象的開銷。雙重檢查鎖是常見的延遲初始化技術,但引入新的問題。
雙重檢查鎖
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) {// 第一次檢查
synchronized (DoubleCheckedLocking.class) {// 加鎖
if (instance == null)// 第二次檢查
instance = new Instance();// 問題根源
}
}
}
}
多線程程序中,讀取到instance不爲null時,instance引用的對象有可能還沒有完成初始化。
問題的根源
instance = new Singleton();這一行代碼可以分解爲3行僞代碼:
memory = allocate(); // 1:分配對象的內存空間
ctorInstance(memory); // 2:初始化對象
instance = memory; // 3:設置instance指向剛分配的內存地址
上面3行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)
memory = allocate(); // 1:分配對象的內存空間
instance = memory; // 3:設置instance指向剛分配的內存地址
// 此時對象還沒有被初始化
ctorInstance(memory); // 2:初始化對象
基於volatile的解決方案
將instance聲明爲volatile類型,就可以實現線程安全的延遲初始化。
volatile禁止指令重排序。
基於類初始化的解決方案
JVM在類的初始化階段(Class被加載後,且被線程使用之前),會執行類的初始化。執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance;
}
}
重排序對其他線程不可見。
Java語言規範規定,對於每一個類或接口C,都有一個唯一的初始化鎖LC與之對應。
參考
- Java併發編程的藝術