雙重檢查鎖與延遲初始化
雙重檢查鎖的錯誤根源?
背景:在Java多線程程序中,有時我們需要採用延遲初始化來降低初始化類和創建對象的開銷,雙重檢查鎖是常見的延遲初始化技術,但是一個錯誤的用法。原因?
- 非線程安全的延遲初始化示例:
public class UnsafeLazyInitialization {
private static ConfigContext instance;
public static ConfigContext getInstance() {
if (instance == null) {
instance = new ConfigContext();
}
return instance;
}
}
- 使用同步實現線程安全的延遲初始化:
public class UnsafeLazyInitialization {
private static ConfigContext instance;
public synchronized static ConfigContext getInstance() {
if (instance == null) {
instance = new ConfigContext();
}
return instance;
}
}
說明:由於synchronized會導致性能開銷,如果上述同步方法被頻繁調用,將導致程序性能下降。
- 雙重檢查鎖降低同步的開銷,實現延遲初始化:
public class DoubleCheckedLocking{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){//代碼1
synchronized (DoubleCheckedLocking.class){
if(instance = null){
instance= new Instance();//代碼2
}
}
}
return instance;
}
}
問題根源
主要在於代碼2處,可以分解成三個動作:
memory = allocate();//1:分配對象內存空間
ctorInstance(memory);//2. 初始化對象
instance = memory; //3. 設置instance指向剛分配的內存地址
以上三個步驟,可能會被重排序:
memory = allocate();//1:分配對象內存空間
instance = memory; //3. 設置instance指向剛分配的內存地址,此時對象還未被初始化
ctorInstance(memory);//2. 初始化對象
此時如果線程a執行到代碼2,線程b支持代碼1判斷到instance不爲null,將繼續使用instance對象,其實instance並未被初始化所以引發異常。
我們找到問題根源,指令重排序,所以我們可以採用手段組織上述 2,3進行重排序,或者重排序對其他線程不可見;
解決方案
- 採用volatile解決
public class DoubleCheckedLocking{
private volatile static Instance instance;
public static Instance getInstance(){
if(instance == null){//代碼1
synchronized (DoubleCheckedLocking.class){
if(instance = null){
instance= new Instance();//代碼2
}
}
}
return instance;
}
}
- 基於類初始化方案
public class InstanceFactory{
private static class InstanceHolder{
public static Instance instance= new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance;//引發InstanceHolder類被初始化,初始化一個類包含執行這個類的靜態初始化和初始化這個類中聲明的靜態字段
}
}
Java語言規範規定,發生下列任意一種情況時,一個類或接口類型T將被初始化。
- T是一個類,而且一個T類型的實例被創建
- T是一個類,且T中聲明的一個靜態方法被調用
- T中聲明的一個靜態字段被賦值
- T中聲明的一個靜態字段被使用,而且這個字段不是一個常量字段
同時Java語言規範規定,每個類或者接口C,都有一個唯一的初始化鎖LC與之對應。