Java中的雙重檢查鎖(double checked locking)
在實作單例模式時,如果未考慮多線程的情況,就容易寫出下面的錯誤代碼:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
在多線程的情況下,這樣寫可能會導緻
uniqueSingleton
有多個執行個體。比如下面這種情況,考慮有兩個線程同時調用
getInstance()
:
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到 為空 | |
T2 | 檢查到 為空 | |
T3 | 初始化對象 | |
T4 | 傳回對象 | |
T5 | 初始化對象 | |
T6 | 傳回對象 |
可以看到,
uniqueSingleton
被執行個體化了兩次并且被不同對象持有。完全違背了單例的初衷。
加鎖
出現這種情況,第一反應就是加鎖,如下:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public synchronized Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
這樣雖然解決了問題,但是因為用到了
synchronized
,會導緻很大的性能開銷,并且加鎖其實隻需要在第一次初始化的時候用到,之後的調用都沒必要再進行加鎖。
雙重檢查鎖
雙重檢查鎖(double checked locking)是對上述問題的一種優化。先判斷對象是否已經被初始化,再決定要不要加鎖。
錯誤的雙重檢查鎖
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
如果這樣寫,運作順序就成了:
- 檢查變量是否被初始化(不去獲得鎖),如果已被初始化則立即傳回。
- 擷取鎖。
- 再次檢查變量是否已經被初始化,如果還沒被初始化就初始化一個對象。
執行雙重檢查是因為,如果多個線程同時了通過了第一次檢查,并且其中一個線程首先通過了第二次檢查并執行個體化了對象,那麼剩餘通過了第一次檢查的線程就不會再去執行個體化對象。
這樣,除了初始化的時候會出現加鎖的情況,後續的所有調用都會避免加鎖而直接傳回,解決了性能消耗的問題。
隐患
上述寫法看似解決了問題,但是有個很大的隐患。執行個體化對象的那行代碼(标記為error的那行),實際上可以分解成以下三個步驟:
- 配置設定記憶體空間
- 初始化對象
- 将對象指向剛配置設定的記憶體空間
但是有些編譯器為了性能的原因,可能會将第二步和第三步進行重排序,順序就成了:
- 配置設定記憶體空間
- 将對象指向剛配置設定的記憶體空間
- 初始化對象
現在考慮重排序後,兩個線程發生了以下調用:
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到 為空 | |
T2 | 擷取鎖 | |
T3 | 再次檢查到 為空 | |
T4 | 為 配置設定記憶體空間 | |
T5 | 将 指向記憶體空間 | |
T6 | 檢查到 不為空 | |
T7 | 通路 (此時對象還未完成初始化) | |
T8 | 初始化 |
在這種情況下,T7時刻線程B對
uniqueSingleton
的通路,通路的是一個初始化未完成的對象。
正确的雙重檢查鎖
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
為了解決上述問題,需要在
uniqueSingleton
前加入關鍵字
volatile
。使用了volatile關鍵字後,重排序被禁止,所有的寫(write)操作都将發生在讀(read)操作之前。
至此,雙重檢查鎖就可以完美工作了。
參考資料:
- 雙重檢查鎖定模式
- 如何在Java中使用雙重檢查鎖實作單例
- 雙重檢查鎖定與延遲初始化