雙重檢查鎖定模式

雙重檢查鎖定模式(也被稱為"雙重檢查加鎖優化","鎖暗示"(Lock hint)[1]) 是一種軟件設計模式用來減少並發系統中競爭和同步的開銷。雙重檢查鎖定模式首先驗證鎖定條件(第一次檢查),只有通過鎖定條件驗證才真正的進行加鎖邏輯並再次驗證條件(第二次檢查)。

該模式在某些語言在某些硬件平台的實現可能是不安全的。有的時候,這一模式被看做是反模式

它通常用於減少加鎖開銷,尤其是為多線程環境中的單例模式實現「惰性初始化」。惰性初始化的意思是直到第一次訪問時才初始化它的值。

Java中的使用

考慮下面的Java代碼[2]頁面存檔備份,存於互聯網檔案館

// Single threaded version
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

這段在使用多線程的情況下無法正常工作。在多個線程同時調用getHelper()時,必須要獲取,否則,這些線程可能同時去創建對象,或者某個線程會得到一個未完全初始化的對象。

鎖可以通過代價很高的同步來獲得,就像下面的例子一樣。

// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

只有getHelper()的第一次調用需要同步創建對象,創建之後getHelper()只是簡單的返回成員變量,而這裏是無需同步的。 由於同步一個方法會降低100倍或更高的性能[2], 每次調用獲取和釋放鎖的開銷似乎是可以避免的:一旦初始化完成,獲取和釋放鎖就顯得很不必要。許多程式設計師以下面這種方式進行優化:

  1. 檢查變量是否被初始化(不去獲得鎖),如果已被初始化立即返回這個變量。
  2. 獲取鎖
  3. 第二次檢查變量是否已經被初始化:如果其他線程曾獲取過鎖,那麼變量已被初始化,返回初始化的變量。
  4. 否則,初始化並返回變量。
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}

直覺上,這個算法看起來像是該問題的有效解決方案。然而,這一技術還有許多需要避免的細微問題。例如,考慮下面的事件序列:

  1. 線程A發現變量沒有被初始化, 然後它獲取鎖並開始變量的初始化。
  2. 由於某些程式語言的語義,編譯器生成的代碼允許在線程A執行完變量的初始化之前,更新變量並將其指向部分初始化的對象。
  3. 線程B發現共享變量已經被初始化,並返回變量。由於線程B確信變量已被初始化,它沒有獲取鎖。如果在A完成初始化之前共享變量對B可見(這是由於A沒有完成初始化或者因為一些初始化的值還沒有覆蓋B使用的內存(緩存一致性)),程序很可能會崩潰。


J2SE 1.4或更早的版本中使用雙重檢查鎖有潛在的危險,有時會正常工作:區分正確實現和有小問題的實現是很困難的。取決於編譯器,線程的調度和其他並發系統活動,不正確的實現雙重檢查鎖導致的異常結果可能會間歇性出現。重現異常是十分困難的。

J2SE 5.0中,這一問題被修正了。volatile關鍵字保證多個線程可以正確處理單件實例。[3]頁面存檔備份,存於互聯網檔案館)描述了這一新的語言特性:

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

注意局部變量result的使用看起來是不必要的。對於某些版本的Java虛擬機,這會使代碼提速25%,而對其他的版本則無關痛癢。[3]

如果helper對象是靜態的(每個類只有一個), 可以使用雙重檢查鎖的替代模式惰性初始化模式[4]。查看[5] 上的列表16.6。

// Correct lazy initialization in Java
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

這是因為內部類直到他們被引用時才會加載。

Java 5中的final語義可以不使用volatile關鍵字實現安全的創建對象:[6]

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper = null;

   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;

      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}

為了正確性,局部變量wrapper是必須的。這一實現的性能不一定比使用volatile的性能更高。

Microsoft Visual C++ 中的使用

如果指針是由C++關鍵字volatile定義的,那麼雙重檢查鎖可以在Visual C++ 2005 或更高版本中實現。Visual C++ 2005 保證volatile變量是一種內存屏障,阻止編譯器和CPU重新安排讀入和寫出語義。[7] 在先前版本的Visual C++則沒有此類保證。在其他方面將指針定義為volatile可能會影響程序的性能。例如,如果指針定義對代碼的其他地方可見,強制編譯器將指針視為屏障,就會降低程序的性能,這是完全不必要的。

參見

參考資料

  1. ^ Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. ^ Boehm, Hans-J. "Threads Cannot Be Implemented As a Library", ACM 2005, p265
  3. ^ Joshua Bloch "Effective Java, Second Edition", p. 283
  4. ^ Brian Goetz et al. Java Concurrency in Practice, 2006 pp348
  5. ^ 存档副本. [2012-02-12]. (原始內容存檔於2012-03-03). 
  6. ^ [1]頁面存檔備份,存於互聯網檔案館) Javamemorymodel-discussion mailing list
  7. ^ 存档副本. [2012-02-12]. (原始內容存檔於2012-10-20). 

外部連結