來源:
Java
並行程式設計的藝術
雙重檢查鎖定的典型例子,如下:
public class DoubleCheckedLocking {
private static Instance instance;
/**
* 雙重檢查鎖定
*/
public static Instance getInstance() {
if (instance == null) { // 1、第一次檢查
synchronized (DoubleCheckedLocking.class) { // 2、加鎖
if (instance == null) { // 3、第二次檢查
instance = new Instance(); // 4、建立物件 (問題的根源在這裡)
}
}
}
return instance;
}
}
這樣的雙重檢查鎖定是一個錯誤的優化!!!
當執行緒讀取到instance
不為null
的時候,instance
參照的物件有可能還沒有完成初始化。
問題的根源在於 4、建立物件
的流程可能會被重排序。
建立物件的流程可以簡化為三步:分配記憶體空間、初始化物件、設定參照指向分配的記憶體地址。
memory = allocate(); // 1、分配物件的記憶體空間
initObject(memory); // 2、初始化物件
instance = memory; // 3、設定instance指向分配的記憶體地址
其中,2 和 3 之間可能被重排序。
memory = allocate(); // 1、分配物件的記憶體空間
instance = memory; // 2、設定instance指向分配的記憶體地址
initObject(memory); // 3、初始化物件
那麼,在發生重排序的情況下,A
、B
兩個執行緒執行這段雙重檢查鎖定的程式碼時的執行順序可能是這樣的:
時間 | A 執行緒 | B 執行緒 |
---|---|---|
t1 | A1:分配物件的記憶體空間 | |
t2 | A2:設定instance指向記憶體空間的地址 | |
t3 | B1:判斷instance是否為null | |
t4 | B2:由於instance不是null,B執行緒將直接存取instance參照的物件 | |
t5 | A3:初始化物件 | |
t6 | A4:存取instance參照的物件 |
按照以上的順序,B
執行緒將會在instance
物件還未初始化完成之前就存取它,導致NPE
。
對於雙重檢查鎖定來實現的延遲初始化方案,只需要將instance
宣告為volatile
型別,就能夠實現執行緒安全的延遲初始化:
public class SafeDoubleCheckedLocking {
private volatile static Instance instance; // 宣告為volatile型別
/**
* 雙重檢查鎖定
*/
public static Instance getInstance() {
if (instance == null) { // 1、第一次檢查
synchronized (DoubleCheckedLocking.class) { // 2、加鎖
if (instance == null) { // 3、第二次檢查
instance = new Instance(); // 4、建立物件
}
}
}
return instance;
}
}
volatile
通過禁止重排序(初始化物件和設定instance
指向分配的記憶體地址)來保證執行緒安全的延遲初始化。
Java
語言規範規定,對於每一個類或者介面,都有唯一的一個初始化鎖與之對應。在多執行緒環境下,只有一個執行緒能夠獲取這個鎖並執行類的初始化,其他執行緒需要等待獲取這個鎖,這樣就能保證執行緒安全的類的初始化。
初始化一個類,包括執行這個類的靜態初始化和初始化在這個類中宣告的靜態欄位。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public Instance getInstance() {
return InstanceHolder.instance; // 觸發InstanceHolder類的初始化
}
}
如上,當執行緒執行getInstance()
方法時,將會觸發InstanceHolder
類的初始化,此時只有一個執行緒能夠獲取InstanceHolder
類的初始化鎖,並完成類的初始化過程,類中的靜態欄位instance
將被初始化,Instance
物件將被建立。雖然Instance
物件建立過程中存在重排序,但是對於其他執行緒來說都是不可見的。
volatile
的延遲初始化方案有一個優勢,就是除了可以對靜態欄位實現延遲初始化之外,還可以對範例欄位實現延遲初始化。volatile
的延遲初始化方案。