單例創(chuàng)建模式是一個(gè)通用的編程習(xí)語。和多線程一起使用時(shí),必需使用某種類型的同步。在努力創(chuàng)建更有效的代碼時(shí),Java 程序員們創(chuàng)建了雙重檢查鎖定習(xí)語,將其和單例創(chuàng)建模式一起使用,從而限制同步代碼量。然而,由于一些不太常見的 Java 內(nèi)存模型細(xì)節(jié)的原因,并不能保證這個(gè)雙重檢查鎖定習(xí)語有效。它偶爾會(huì)失敗,而不是總失敗。此外,它失敗的原因并不明顯,還包含 Java 內(nèi)存模型的一些隱秘細(xì)節(jié)。這些事實(shí)將導(dǎo)致代碼失敗,原因是雙重檢查鎖定難于跟蹤。在本文余下的部分里,我們將詳細(xì)介紹雙重檢查鎖定習(xí)語,從而理解它在何處失效。
單例創(chuàng)建習(xí)語
要理解雙重檢查鎖定習(xí)語是從哪里起源的,就必須理解通用單例創(chuàng)建習(xí)語,如清單 1 中的闡釋:
清單 1. 單例創(chuàng)建習(xí)語
import java.util.*;
class Singleton
{
private static Singleton instance;
private Vector v;
private boolean inUse;
private Singleton()
{
v = new Vector();
v.addElement(new Object());
inUse = true;
}
public static Singleton getInstance()
{
if (instance == null) //1
instance = new Singleton(); //2
return instance; //3
}
}
|
此類的設(shè)計(jì)確保只創(chuàng)建一個(gè) Singleton
對象。構(gòu)造函數(shù)被聲明為 private
,getInstance()
方法只創(chuàng)建一個(gè)對象。這個(gè)實(shí)現(xiàn)適合于單線程程序。然而,當(dāng)引入多線程時(shí),就必須通過同步來保護(hù) getInstance()
方法。如果不保護(hù) getInstance()
方法,則可能返回 Singleton
對象的兩個(gè)不同的實(shí)例。假設(shè)兩個(gè)線程并發(fā)調(diào)用 getInstance()
方法并且按以下順序執(zhí)行調(diào)用:
- 線程 1 調(diào)用
getInstance()
方法并決定 instance
在 //1 處為 null
。
- 線程 1 進(jìn)入
if
代碼塊,但在執(zhí)行 //2 處的代碼行時(shí)被線程 2 預(yù)占。
- 線程 2 調(diào)用
getInstance()
方法并在 //1 處決定 instance
為 null
。
- 線程 2 進(jìn)入
if
代碼塊并創(chuàng)建一個(gè)新的 Singleton
對象并在 //2 處將變量 instance
分配給這個(gè)新對象。
- 線程 2 在 //3 處返回
Singleton
對象引用。
- 線程 2 被線程 1 預(yù)占。
- 線程 1 在它停止的地方啟動(dòng),并執(zhí)行 //2 代碼行,這導(dǎo)致創(chuàng)建另一個(gè)
Singleton
對象。
- 線程 1 在 //3 處返回這個(gè)對象。
結(jié)果是 getInstance()
方法創(chuàng)建了兩個(gè) Singleton
對象,而它本該只創(chuàng)建一個(gè)對象。通過同步 getInstance()
方法從而在同一時(shí)間只允許一個(gè)線程執(zhí)行代碼,這個(gè)問題得以改正,如清單 2 所示:
清單 2. 線程安全的 getInstance() 方法
public static synchronized Singleton getInstance()
{
if (instance == null) //1
instance = new Singleton(); //2
return instance; //3
}
|
清單 2 中的代碼針對多線程訪問 getInstance()
方法運(yùn)行得很好。然而,當(dāng)分析這段代碼時(shí),您會(huì)意識到只有在第一次調(diào)用方法時(shí)才需要同步。由于只有第一次調(diào)用執(zhí)行了 //2 處的代碼,而只有此行代碼需要同步,因此就無需對后續(xù)調(diào)用使用同步。所有其他調(diào)用用于決定 instance
是非 null
的,并將其返回。多線程能夠安全并發(fā)地執(zhí)行除第一次調(diào)用外的所有調(diào)用。盡管如此,由于該方法是 synchronized
的,需要為該方法的每一次調(diào)用付出同步的代價(jià),即使只有第一次調(diào)用需要同步。
為使此方法更為有效,一個(gè)被稱為雙重檢查鎖定的習(xí)語就應(yīng)運(yùn)而生了。這個(gè)想法是為了避免對除第一次調(diào)用外的所有調(diào)用都實(shí)行同步的昂貴代價(jià)。同步的代價(jià)在不同的 JVM 間是不同的。在早期,代價(jià)相當(dāng)高。隨著更高級的 JVM 的出現(xiàn),同步的代價(jià)降低了,但出入 synchronized
方法或塊仍然有性能損失。不考慮 JVM 技術(shù)的進(jìn)步,程序員們絕不想不必要地浪費(fèi)處理時(shí)間。
因?yàn)橹挥星鍐?2 中的 //2 行需要同步,我們可以只將其包裝到一個(gè)同步塊中,如清單 3 所示:
清單 3. getInstance() 方法
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
|
清單 3 中的代碼展示了用多線程加以說明的和清單 1 相同的問題。當(dāng) instance
為 null
時(shí),兩個(gè)線程可以并發(fā)地進(jìn)入 if
語句內(nèi)部。然后,一個(gè)線程進(jìn)入 synchronized
塊來初始化 instance
,而另一個(gè)線程則被阻斷。當(dāng)?shù)谝粋€(gè)線程退出 synchronized
塊時(shí),等待著的線程進(jìn)入并創(chuàng)建另一個(gè) Singleton
對象。注意:當(dāng)?shù)诙€(gè)線程進(jìn)入 synchronized
塊時(shí),它并沒有檢查 instance
是否非 null
。
雙重檢查鎖定
為處理清單 3 中的問題,我們需要對 instance
進(jìn)行第二次檢查。這就是“雙重檢查鎖定”名稱的由來。將雙重檢查鎖定習(xí)語應(yīng)用到清單 3 的結(jié)果就是清單 4 。
清單 4. 雙重檢查鎖定示例
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}
|
雙重檢查鎖定背后的理論是:在 //2 處的第二次檢查使(如清單 3 中那樣)創(chuàng)建兩個(gè)不同的 Singleton
對象成為不可能。假設(shè)有下列事件序列:
- 線程 1 進(jìn)入
getInstance()
方法。
- 由于
instance
為 null
,線程 1 在 //1 處進(jìn)入 synchronized
塊。
- 線程 1 被線程 2 預(yù)占。
- 線程 2 進(jìn)入
getInstance()
方法。
- 由于
instance
仍舊為 null
,線程 2 試圖獲取 //1 處的鎖。然而,由于線程 1 持有該鎖,線程 2 在 //1 處阻塞。
- 線程 2 被線程 1 預(yù)占。
- 線程 1 執(zhí)行,由于在 //2 處實(shí)例仍舊為
null
,線程 1 還創(chuàng)建一個(gè) Singleton
對象并將其引用賦值給 instance
。
- 線程 1 退出
synchronized
塊并從 getInstance()
方法返回實(shí)例。
- 線程 1 被線程 2 預(yù)占。
- 線程 2 獲取 //1 處的鎖并檢查
instance
是否為 null
。
- 由于
instance
是非 null
的,并沒有創(chuàng)建第二個(gè) Singleton
對象,由線程 1 創(chuàng)建的對象被返回。
雙重檢查鎖定背后的理論是完美的。不幸地是,現(xiàn)實(shí)完全不同。雙重檢查鎖定的問題是:并不能保證它會(huì)在單處理器或多處理器計(jì)算機(jī)上順利運(yùn)行。
雙重檢查鎖定失敗的問題并不歸咎于 JVM 中的實(shí)現(xiàn) bug,而是歸咎于 Java 平臺內(nèi)存模型。內(nèi)存模型允許所謂的“無序?qū)懭?#8221;,這也是這些習(xí)語失敗的一個(gè)主要原因。
無序?qū)懭?/span>
為解釋該問題,需要重新考察上述清單 4 中的 //3 行。此行代碼創(chuàng)建了一個(gè) Singleton
對象并初始化變量 instance
來引用此對象。這行代碼的問題是:在 Singleton
構(gòu)造函數(shù)體執(zhí)行之前,變量 instance
可能成為非 null
的。
什么?這一說法可能讓您始料未及,但事實(shí)確實(shí)如此。在解釋這個(gè)現(xiàn)象如何發(fā)生前,請先暫時(shí)接受這一事實(shí),我們先來考察一下雙重檢查鎖定是如何被破壞的。假設(shè)清單 4 中代碼執(zhí)行以下事件序列:
- 線程 1 進(jìn)入
getInstance()
方法。
- 由于
instance
為 null
,線程 1 在 //1 處進(jìn)入 synchronized
塊。
- 線程 1 前進(jìn)到 //3 處,但在構(gòu)造函數(shù)執(zhí)行之前,使實(shí)例成為非
null
。
- 線程 1 被線程 2 預(yù)占。
- 線程 2 檢查實(shí)例是否為
null
。因?yàn)閷?shí)例不為 null,線程 2 將 instance
引用返回給一個(gè)構(gòu)造完整但部分初始化了的 Singleton
對象。
- 線程 2 被線程 1 預(yù)占。
- 線程 1 通過運(yùn)行
Singleton
對象的構(gòu)造函數(shù)并將引用返回給它,來完成對該對象的初始化。
此事件序列發(fā)生在線程 2 返回一個(gè)尚未執(zhí)行構(gòu)造函數(shù)的對象的時(shí)候。
為展示此事件的發(fā)生情況,假設(shè)為代碼行 instance =new Singleton();
執(zhí)行了下列偽代碼: instance =new Singleton();
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing
//instance.
|
這段偽代碼不僅是可能的,而且是一些 JIT 編譯器上真實(shí)發(fā)生的。執(zhí)行的順序是顛倒的,但鑒于當(dāng)前的內(nèi)存模型,這也是允許發(fā)生的。JIT 編譯器的這一行為使雙重檢查鎖定的問題只不過是一次學(xué)術(shù)實(shí)踐而已。
為說明這一情況,假設(shè)有清單 5 中的代碼。它包含一個(gè)剝離版的 getInstance()
方法。我已經(jīng)刪除了“雙重檢查性”以簡化我們對生成的匯編代碼(清單 6)的回顧。我們只關(guān)心 JIT 編譯器如何編譯 instance=new Singleton();
代碼。此外,我提供了一個(gè)簡單的構(gòu)造函數(shù)來明確說明匯編代碼中該構(gòu)造函數(shù)的運(yùn)行情況。
清單 5. 用于演示無序?qū)懭氲膯卫?/strong>
class Singleton
{
private static Singleton instance;
private boolean inUse;
private int val;
private Singleton()
{
inUse = true;
val = 5;
}
public static Singleton getInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
|
清單 6 包含由 Sun JDK 1.2.1 JIT 編譯器為清單 5 中的 getInstance()
方法體生成的匯編代碼。
清單 6. 由清單 5 中的代碼生成的匯編代碼
;asm code generated for getInstance
054D20B0 mov eax,[049388C8] ;load instance ref
054D20B5 test eax,eax ;test for null
054D20B7 jne 054D20D7
054D20B9 mov eax,14C0988h
054D20BE call 503EF8F0 ;allocate memory
054D20C3 mov [049388C8],eax ;store pointer in
;instance ref. instance
;non-null and ctor
;has not run
054D20C8 mov ecx,dword ptr [eax]
054D20CA mov dword ptr [ecx],1 ;inline ctor - inUse=true;
054D20D0 mov dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7 mov ebx,dword ptr ds:[49388C8h]
054D20DD jmp 054D20B0
|
注: 為引用下列說明中的匯編代碼行,我將引用指令地址的最后兩個(gè)值,因?yàn)樗鼈兌家?054D20
開頭。例如,B5
代表 test eax,eax
。
匯編代碼是通過運(yùn)行一個(gè)在無限循環(huán)中調(diào)用 getInstance()
方法的測試程序來生成的。程序運(yùn)行時(shí),請運(yùn)行 Microsoft Visual C++ 調(diào)試器并將其附到表示測試程序的 Java 進(jìn)程中。然后,中斷執(zhí)行并找到表示該無限循環(huán)的匯編代碼。
B0
和 B5
處的前兩行匯編代碼將 instance
引用從內(nèi)存位置 049388C8
加載至 eax
中,并進(jìn)行 null
檢查。這跟清單 5 中的 getInstance()
方法的第一行代碼相對應(yīng)。第一次調(diào)用此方法時(shí),instance
為 null
,代碼執(zhí)行到 B9
。BE
處的代碼為 Singleton
對象從堆中分配內(nèi)存,并將一個(gè)指向該塊內(nèi)存的指針存儲到 eax
中。下一行代碼,C3
,獲取 eax
中的指針并將其存儲回內(nèi)存位置為 049388C8
的實(shí)例引用。結(jié)果是,instance
現(xiàn)在為非 null
并引用一個(gè)有效的 Singleton
對象。然而,此對象的構(gòu)造函數(shù)尚未運(yùn)行,這恰是破壞雙重檢查鎖定的情況。然后,在 C8
行處,instance
指針被解除引用并存儲到 ecx
。CA
和 D0
行表示內(nèi)聯(lián)的構(gòu)造函數(shù),該構(gòu)造函數(shù)將值 true
和 5
存儲到 Singleton
對象。如果此代碼在執(zhí)行 C3
行后且在完成該構(gòu)造函數(shù)前被另一個(gè)線程中斷,則雙重檢查鎖定就會(huì)失敗。
不是所有的 JIT 編譯器都生成如上代碼。一些生成了代碼,從而只在構(gòu)造函數(shù)執(zhí)行后使 instance
成為非 null
。針對 Java 技術(shù)的 IBM SDK 1.3 版和 Sun JDK 1.3 都生成這樣的代碼。然而,這并不意味著應(yīng)該在這些實(shí)例中使用雙重檢查鎖定。該習(xí)語失敗還有一些其他原因。此外,您并不總能知道代碼會(huì)在哪些 JVM 上運(yùn)行,而 JIT 編譯器總是會(huì)發(fā)生變化,從而生成破壞此習(xí)語的代碼。
雙重檢查鎖定:獲取兩個(gè)
考慮到當(dāng)前的雙重檢查鎖定不起作用,我加入了另一個(gè)版本的代碼,如清單 7 所示,從而防止您剛才看到的無序?qū)懭雴栴}。
清單 7. 解決無序?qū)懭雴栴}的嘗試
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
inst = new Singleton(); //4
}
instance = inst; //5
}
}
}
return instance;
}
|
看著清單 7 中的代碼,您應(yīng)該意識到事情變得有點(diǎn)荒謬。請記住,創(chuàng)建雙重檢查鎖定是為了避免對簡單的三行 getInstance()
方法實(shí)現(xiàn)同步。清單 7 中的代碼變得難于控制。另外,該代碼沒有解決問題。仔細(xì)檢查可獲悉原因。
此代碼試圖避免無序?qū)懭雴栴}。它試圖通過引入局部變量 inst
和第二個(gè) synchronized
塊來解決這一問題。該理論實(shí)現(xiàn)如下:
- 線程 1 進(jìn)入
getInstance()
方法。
- 由于
instance
為 null
,線程 1 在 //1 處進(jìn)入第一個(gè) synchronized
塊。
- 局部變量
inst
獲取 instance
的值,該值在 //2 處為 null
。
- 由于
inst
為 null
,線程 1 在 //3 處進(jìn)入第二個(gè) synchronized
塊。
- 線程 1 然后開始執(zhí)行 //4 處的代碼,同時(shí)使
inst
為非 null
,但在 Singleton
的構(gòu)造函數(shù)執(zhí)行前。(這就是我們剛才看到的無序?qū)懭雴栴}。)
- 線程 1 被線程 2 預(yù)占。
- 線程 2 進(jìn)入
getInstance()
方法。
- 由于
instance
為 null
,線程 2 試圖在 //1 處進(jìn)入第一個(gè) synchronized
塊。由于線程 1 目前持有此鎖,線程 2 被阻斷。
- 線程 1 然后完成 //4 處的執(zhí)行。
- 線程 1 然后將一個(gè)構(gòu)造完整的
Singleton
對象在 //5 處賦值給變量 instance
,并退出這兩個(gè) synchronized
塊。
- 線程 1 返回
instance
。
- 然后執(zhí)行線程 2 并在 //2 處將
instance
賦值給 inst
。
- 線程 2 發(fā)現(xiàn)
instance
為非 null
,將其返回。
這里的關(guān)鍵行是 //5。此行應(yīng)該確保 instance
只為 null
或引用一個(gè)構(gòu)造完整的 Singleton
對象。該問題發(fā)生在理論和實(shí)際彼此背道而馳的情況下。
由于當(dāng)前內(nèi)存模型的定義,清單 7 中的代碼無效。Java 語言規(guī)范(Java Language Specification,JLS)要求不能將 synchronized
塊中的代碼移出來。但是,并沒有說不能將 synchronized
塊外面的代碼移入 synchronized
塊中。
JIT 編譯器會(huì)在這里看到一個(gè)優(yōu)化的機(jī)會(huì)。此優(yōu)化會(huì)刪除 //4 和 //5 處的代碼,組合并且生成清單 8 中所示的代碼。
清單 8. 從清單 7 中優(yōu)化來的代碼。
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
//inst = new Singleton(); //4
instance = new Singleton();
}
//instance = inst; //5
}
}
}
return instance;
}
|
如果進(jìn)行此項(xiàng)優(yōu)化,您將同樣遇到我們之前討論過的無序?qū)懭雴栴}。
用 volatile 聲明每一個(gè)變量怎么樣?
另一個(gè)想法是針對變量 inst
以及 instance
使用關(guān)鍵字 volatile
。根據(jù) JLS(參見 參考資料),聲明成 volatile
的變量被認(rèn)為是順序一致的,即,不是重新排序的。但是試圖使用 volatile
來修正雙重檢查鎖定的問題,會(huì)產(chǎn)生以下兩個(gè)問題:
- 這里的問題不是有關(guān)順序一致性的,而是代碼被移動(dòng)了,不是重新排序。
- 即使考慮了順序一致性,大多數(shù)的 JVM 也沒有正確地實(shí)現(xiàn)
volatile
。
第二點(diǎn)值得展開討論。假設(shè)有清單 9 中的代碼:
清單 9. 使用了 volatile 的順序一致性
class test
{
private volatile boolean stop = false;
private volatile int num = 0;
public void foo()
{
num = 100; //This can happen second
stop = true; //This can happen first
//...
}
public void bar()
{
if (stop)
num += num; //num can == 0!
}
//...
}
|
根據(jù) JLS,由于 stop
和 num
被聲明為 volatile
,它們應(yīng)該順序一致。這意味著如果 stop
曾經(jīng)是 true
,num
一定曾被設(shè)置成 100
。盡管如此,因?yàn)樵S多 JVM 沒有實(shí)現(xiàn) volatile
的順序一致性功能,您就不能依賴此行為。因此,如果線程 1 調(diào)用 foo
并且線程 2 并發(fā)地調(diào)用 bar
,則線程 1 可能在 num
被設(shè)置成為 100
之前將 stop
設(shè)置成 true
。這將導(dǎo)致線程見到 stop
是 true
,而 num
仍被設(shè)置成 0
。使用 volatile
和 64 位變量的原子數(shù)還有另外一些問題,但這已超出了本文的討論范圍。有關(guān)此主題的更多信息,請參閱 參考資料。
解決方案
底線就是:無論以何種形式,都不應(yīng)使用雙重檢查鎖定,因?yàn)槟荒鼙WC它在任何 JVM 實(shí)現(xiàn)上都能順利運(yùn)行。JSR-133 是有關(guān)內(nèi)存模型尋址問題的,盡管如此,新的內(nèi)存模型也不會(huì)支持雙重檢查鎖定。因此,您有兩種選擇:
- 接受如清單 2 中所示的
getInstance()
方法的同步。
- 放棄同步,而使用一個(gè)
static
字段。
選擇項(xiàng) 2 如清單 10 中所示
清單 10. 使用 static 字段的單例實(shí)現(xiàn)
class Singleton
{
private Vector v;
private boolean inUse;
private static Singleton instance = new Singleton();
private Singleton()
{
v = new Vector();
inUse = true;
//...
}
public static Singleton getInstance()
{
return instance;
}
}
|
清單 10 的代碼沒有使用同步,并且確保調(diào)用 static getInstance()
方法時(shí)才創(chuàng)建 Singleton
。如果您的目標(biāo)是消除同步,則這將是一個(gè)很好的選擇。
String 不是不變的
鑒于無序?qū)懭牒鸵迷跇?gòu)造函數(shù)執(zhí)行前變成非 null
的問題,您可能會(huì)考慮 String
類。假設(shè)有下列代碼:
private String str;
//...
str = new String("hello");
|
String
類應(yīng)該是不變的。盡管如此,鑒于我們之前討論的無序?qū)懭雴栴},那會(huì)在這里導(dǎo)致問題嗎?答案是肯定的。考慮兩個(gè)線程訪問 String str
。一個(gè)線程能看見 str
引用一個(gè) String
對象,在該對象中構(gòu)造函數(shù)尚未運(yùn)行。事實(shí)上,清單 11 包含展示這種情況發(fā)生的代碼。注意,這個(gè)代碼僅在我測試用的舊版 JVM 上會(huì)失敗。IBM 1.3 和 Sun 1.3 JVM 都會(huì)如期生成不變的 String
。
清單 11. 可變 String 的例子
class StringCreator extends Thread
{
MutableString ms;
public StringCreator(MutableString muts)
{
ms = muts;
}
public void run()
{
while(true)
ms.str = new String("hello"); //1
}
}
class StringReader extends Thread
{
MutableString ms;
public StringReader(MutableString muts)
{
ms = muts;
}
public void run()
{
while(true)
{
if (!(ms.str.equals("hello"))) //2
{
System.out.println("String is not immutable!");
break;
}
}
}
}
class MutableString
{
public String str; //3
public static void main(String args[])
{
MutableString ms = new MutableString(); //4
new StringCreator(ms).start(); //5
new StringReader(ms).start(); //6
}
}
|
此代碼在 //4 處創(chuàng)建一個(gè) MutableString
類,它包含了一個(gè) String
引用,此引用由 //3 處的兩個(gè)線程共享。在行 //5 和 //6 處,在兩個(gè)分開的線程上創(chuàng)建了兩個(gè)對象 StringCreator
和 StringReader
。傳入一個(gè) MutableString
對象的引用。StringCreator
類進(jìn)入到一個(gè)無限循環(huán)中并且使用值“hello”在 //1 處創(chuàng)建 String
對象。StringReader
也進(jìn)入到一個(gè)無限循環(huán)中,并且在 //2 處檢查當(dāng)前的 String
對象的值是不是 “hello”。如果不行,StringReader
線程打印出一條消息并停止。如果 String
類是不變的,則從此程序應(yīng)當(dāng)看不到任何輸出。如果發(fā)生了無序?qū)懭雴栴},則使 StringReader
看到 str
引用的惟一方法絕不是值為“hello”的 String
對象。
在舊版的 JVM 如 Sun JDK 1.2.1 上運(yùn)行此代碼會(huì)導(dǎo)致無序?qū)懭雴栴}。并因此導(dǎo)致一個(gè)非不變的 String
。
結(jié)束語
為避免單例中代價(jià)高昂的同步,程序員非常聰明地發(fā)明了雙重檢查鎖定習(xí)語。不幸的是,鑒于當(dāng)前的內(nèi)存模型的原因,該習(xí)語尚未得到廣泛使用,就明顯成為了一種不安全的編程結(jié)構(gòu)。重定義脆弱的內(nèi)存模型這一領(lǐng)域的工作正在進(jìn)行中。盡管如此,即使是在新提議的內(nèi)存模型中,雙重檢查鎖定也是無效的。對此問題最佳的解決方案是接受同步或者使用一個(gè) static field
。