雙重檢查鎖定模式

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

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

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

Java 中的使用

考慮下面的 Java 程式碼

// 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 倍或更高的效能， 每次呼叫獲取和釋放鎖的開銷似乎是可以避免的：一旦初始化完成，獲取和釋放鎖就顯得很不必要。許多程式設計師以下面這種方式進行最佳化：

檢查變數是否被初始化 (不去獲得鎖)，如果已被初始化立即返回這個變數。
獲取鎖
第二次檢查變數是否已經被初始化：如果其他執行緒曾獲取過鎖，那麼變數已被初始化，返回初始化的變數。
否則，初始化並返回變數。

// 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...

}

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

執行緒 A 發現變數沒有被初始化，然後它獲取鎖並開始變數的初始化。
由於某些程式語言的語義，編譯器生成的程式碼允許線上程 A 執行完變數的初始化之前，更新變數並將其指向部分初始化的物件。
執行緒 B 發現共享變數已經被初始化，並返回變數。由於執行緒 B 確信變數已被初始化，它沒有獲取鎖。如果在 A 完成初始化之前共享變數對 B 可見（這是由於 A 沒有完成初始化或者因為一些初始化的值還沒有覆蓋 B 使用的記憶體 (快取一致性)），程式很可能會崩潰。

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

在 J2SE 5.0 中，這一問題被修正了。volatile 關鍵字保證多個執行緒可以正確處理單件例項。描述了這一新的語言特性:

// 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%，而對其他的版本則無關痛癢。

如果 helper 物件是靜態的 (每個類只有一個), 可以使用雙重檢查鎖的替代模式惰性初始化模式。檢視 上的列表 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 關鍵字實現安全的建立物件：

public class FinalWrapper {

public final T value;

public FinalWrapper (T value) {this.value = value;}

}

public class Foo {

private FinalWrapper helperWrapper = null;

public Helper getHelper {

FinalWrapper wrapper = helperWrapper;

if (wrapper == null) {synchronized (this) {if (helperWrapper == null) {helperWrapper = new FinalWrapper (new Helper );

}

wrapper = helperWrapper;

}

}

return wrapper.value;

}

}

為了正確性，區域性變數 wrapper 是必須的。這一實現的效能不一定比使用 volatile 的效能更高。

Microsoft Visual C++ 中的使用

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