天天看點

Java單例模式之雙重檢查鎖的應用Java中的雙重檢查鎖(double checked locking)加鎖雙重檢查鎖

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 檢查到

uniqueSingleton

為空
T2 檢查到

uniqueSingleton

為空
T3 初始化對象

A

T4 傳回對象

A

T5 初始化對象

B

T6 傳回對象

B

可以看到,

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;
    }
}
           

如果這樣寫,運作順序就成了:

  1. 檢查變量是否被初始化(不去獲得鎖),如果已被初始化則立即傳回。
  2. 擷取鎖。
  3. 再次檢查變量是否已經被初始化,如果還沒被初始化就初始化一個對象。

執行雙重檢查是因為,如果多個線程同時了通過了第一次檢查,并且其中一個線程首先通過了第二次檢查并執行個體化了對象,那麼剩餘通過了第一次檢查的線程就不會再去執行個體化對象。

這樣,除了初始化的時候會出現加鎖的情況,後續的所有調用都會避免加鎖而直接傳回,解決了性能消耗的問題。

隐患

上述寫法看似解決了問題,但是有個很大的隐患。執行個體化對象的那行代碼(标記為error的那行),實際上可以分解成以下三個步驟:

  1. 配置設定記憶體空間
  2. 初始化對象
  3. 将對象指向剛配置設定的記憶體空間

但是有些編譯器為了性能的原因,可能會将第二步和第三步進行重排序,順序就成了:

  1. 配置設定記憶體空間
  2. 将對象指向剛配置設定的記憶體空間
  3. 初始化對象

現在考慮重排序後,兩個線程發生了以下調用:

Time Thread A Thread B
T1 檢查到

uniqueSingleton

為空
T2 擷取鎖
T3 再次檢查到

uniqueSingleton

為空
T4

uniqueSingleton

配置設定記憶體空間
T5

uniqueSingleton

指向記憶體空間
T6 檢查到

uniqueSingleton

不為空
T7 通路

uniqueSingleton

(此時對象還未完成初始化)
T8 初始化

uniqueSingleton

在這種情況下,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)操作之前。

至此,雙重檢查鎖就可以完美工作了。

參考資料:

  1. 雙重檢查鎖定模式
  2. 如何在Java中使用雙重檢查鎖實作單例
  3. 雙重檢查鎖定與延遲初始化