Java中的雙重檢查鎖(double checked locking)

最初的代碼

在最近的項目中,寫出了這樣的一段代碼

private static SomeClass    instance;

public SomeClass getInstance() {
    if (null == instance) {
        instance = new SomeClass();
    }
    return instance;
}

然後在Code Review的時候被告知在多線程的情況下,這樣寫可能會導致instance有多個實例。比如下面這種情況:

Time Thread A Thread B
t1 A1 檢查到instance爲空
t2 B1 檢查到instance爲空
t3 B2 初始化對象
t4 A2 初始化對象

第一次的解決方案

於是就想到了爲這個方法加鎖,如下:

private static SomeClass    instance;

public synchronized SomeClass getInstance() {
    if (null == instance) {
        instance = new SomeClass();
    }
    return instance;
}

但是又被提醒這樣雖然解決了問題,但是會導致很大的性能開銷,而加鎖只需要在第一次初始化的時候用到,之後的調用都沒必要再進行加鎖,於是就瞭解到了雙重檢查鎖(double checked locking)的辦法。

第二次的解決方案

private static SomeClass    instance;

public SomeClass getInstance() {
    if (null == instance) {                     // 第一重檢查
        synchronized (this) {
            if (null == instance) {             // 第二重檢查
                instance = new SomeClass();     // 這裏有問題
            }
        }
    }
    return instance;
}

這樣寫的話,運行順序就成了:

  1. 檢查變量是否被初始化(不去獲得鎖),如果已被初始化立即返回這個變量。
  2. 獲取鎖
  3. 第二次檢查變量是否已經被初始化:如果其他線程曾獲取過鎖,那麼變量已被初始化,返回初始化的變量。否則,初始化並返回變量。

這樣,除了初始化的時候會出現加鎖的情況,後續的所有調用都會避免加鎖而直接返回,從而避免了性能問題,而且看似也解決了同步的問題,然而這樣寫有個很大的隱患。詳細原因如下:

實例化對象的那行代碼(標記爲有問題的那行),實際上可以分解成以下三個步驟:

  1. 分配內存空間
  2. 初始化對象
  3. 將對象指向剛分配的內存空間

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

  1. 分配內存空間
  2. 將對象指向剛分配的內存空間
  3. 初始化對象

現在考慮重排序後,發生了以下這種調用:

Time Thread A Thread B
t1 A1 檢查到instance爲空
t2 A2 獲取鎖
t3 A3 再次檢查到instance爲空
t4 A4 爲instance分配內存空間
t5 A5 將instance指向內存空間
t6 B1 檢查到instance不爲空
t7 B2 訪問instance(對象還未初始化)
t8 A6 初始化instance

注意,在這種情況下,t7時刻線程B對instance的訪問,訪問的是一個初始化未完成的對象!

最終的解決方案

爲了解決上述問題,可以在instance前加入關鍵字volatile。使用了volatile變量後,就能保證先行發生關。對於volatile變量,所有的寫(write)都將先行發生於讀(read),但在Java5之前不是這樣,所以這樣的方法只針對Java5及以上的版本。

private volatile static SomeClass    instance;

public SomeClass getInstance() {
    if (null == instance) {
        synchronized (Test.class) {
            if (null == instance) {
                instance = new SomeClass();
            }
        }
    }
    return instance;
}

至此,雙重檢查鎖就可以完美工作了。現在實現單例模式也有了別的更好的辦法,但個人覺得這樣的坑很有教育意義,故做此記錄。

參考資料:

  1. 雙重檢查鎖定模式
  2. 如何在Java中使用雙重檢查鎖實現單例
  3. 雙重檢查鎖定與延遲初始化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章