前幾天在看一段.NET源代碼的時候偶爾遇到了Double-checked Locking (雙檢鎖)的一個使用,于是想到了以前看過的一些資料,寫出來分享一下。
主要參考:The "Double-Checked Locking is Broken" Delaration (http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)
雙檢鎖是在多線程環(huán)境下很常見的一種實現(xiàn)singleton模式里lazy initialization的方法。
先看一下最這個模式的起源(注:代碼為Java,不過這個問題適用各種語言,比如C++):
// Single threaded version
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// other functions and members...
}
很容易看出,
i件軟]TRv管7l;YU+y管T
在多線程的情況下,上面的getHelper是不能正確工作的(可能生成多個helper實體)。
于是有下面的改進(jìn)代碼:
// Correct multithreaded version
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// other functions and members...
}
這樣寫程序不會出錯,教s網(wǎng)~XiH~\D\n因為整個getHelper是一個整體的"critical section",但就是效率很不好,因為我們的目的其實只是在第一個初始化helper的時候需要locking(加鎖),而后面取用helper的時候,根本不需要線程同步。
于是聰明的人們想出了下面的做法:
// 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...
}
思路很簡單,就是我們只需要同步(synchronize)初始化helper的那部分代碼從而使代碼既正確又很有效率。
這就是所謂的“雙檢鎖”機(jī)制(顧名思義)。
很可惜,這樣的寫法在很多平臺和優(yōu)化編譯器上是錯誤的。
原因在于:helper = new Helper()這行代碼在不同編譯器上的行為是無法預(yù)知的。一個優(yōu)化編譯器可以合法地如下實現(xiàn)helper = new Helper():
1. helper = 給新的實體分配內(nèi)存
2. 調(diào)用helper的構(gòu)造函數(shù)來初始化helper的成員變量
現(xiàn)在想象一下有線程A和B在調(diào)用getHelper,
5LG2]專&件9w
線程A先進(jìn)入,在執(zhí)行到步驟1的時候被踢出了cpu。然后線程B進(jìn)入,B看到的是helper 已經(jīng)不是null了(內(nèi)存已經(jīng)分配),于是它開始放心地使用helper,但這個是錯誤的,因為在這一時刻,helper的成員變量還都是缺省值,A還沒 有來得及執(zhí)行步驟2來完成helper的初始化。
當(dāng)然編譯器也可以這樣實現(xiàn):
1. temp = 分配內(nèi)存
2. 調(diào)用temp的構(gòu)造函數(shù)
3. helper = temp
如果編譯器的行為是這樣的話我們似乎就沒有問題了,但事實卻不是那么簡單,因為我們無法知道某個編譯器具體是怎么做的,因為在Java的 memory model里對這個問題沒有定義(C++也一樣),而事實上有很多編譯器都是用第一種方法(比如symantec的just-in-time compiler),因為第一種方法看起來更自然。
在上面的參考文章中還提到了更復(fù)雜的修改方法,不過很可惜,都是錯誤的,我這里就略去不介紹了。
那么有什么解決方案呢?有如下一些:
1. 如果你的singleton是static的,那你可以將這個singleton申明為一個獨立類的一個成員變量:
class HelperSingleton {
static Helper singleton = new Helper();
}
Java的語意會保證:1. lazy initialization, 2. singleton在被調(diào)用前已經(jīng)完全初始化了。
2. 雙檢鎖對于基礎(chǔ)類型(比如int)適用。很顯然吧,因為基礎(chǔ)類型沒有調(diào)用構(gòu)造函數(shù)這一步。事實上,我前面提到的.NET里面的那段代碼就是在一個int變量上使用雙檢鎖。
3. 使用explicit memory barrier。這個我不說了,關(guān)于memory barrier我們可以寫一本小冊子來介紹,有興趣的朋友可以自己查一下資料,上面的參考里也有很多相關(guān)鏈接。
4. 使用Thread Local Storage。也不介紹了。
上面的文章還提到了Java在考慮為volatile關(guān)鍵字定義新的語意來解決這個問題以及雙檢鎖對Java里immutable對象影響,
fh?件e"教40業(yè)
不過因 為這篇文章已經(jīng)有些年頭而我也不是Java的專家,所以不太清楚現(xiàn)在的情況怎樣,總之,在遇到雙檢鎖的時候,需要的朋友應(yīng)該做些必要的調(diào)查來確定自己的代 碼是線程安全的。