|
<!----><!----><!---->
|
級別: 中級
Peter Haggar
, 高級軟件工程師, IBM
2004 年 5 月 01 日
所 有的編程語言都有一些共用的習(xí)語。了解和使用一些習(xí)語很有用,程序員們花費(fèi)寶貴的時(shí)間來創(chuàng)建、學(xué)習(xí)和實(shí)現(xiàn)這些習(xí)語。問題是,稍后經(jīng)過證明,一些習(xí)語并不完 全如其所聲稱的那樣,或者僅僅是與描述的功能不符。在 Java 編程語言中,雙重檢查鎖定就是這樣的一個(gè)絕不應(yīng)該使用的習(xí)語。在本文中,Peter Haggar 介紹了雙重檢查鎖定習(xí)語的淵源,開發(fā)它的原因和它失效的原因。
<!----><!----> <!---->
編輯注
:本文在針對 Java 5.0 修訂前參考了 Java 內(nèi)存模型;關(guān)于內(nèi)存排序的描述也許不再正確。盡管如此,在新的內(nèi)存模型中,雙重檢查鎖定習(xí)語仍舊是無效的。
單 例創(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ì)意識(shí)到只有在第一次調(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 平臺(tái)內(nèi)存模型。內(nèi)存模型允許所謂的“無序?qū)懭搿保@也是這些習(xí)語失敗的一個(gè)主要原因。
無序?qū)懭?
為解釋該問題,需要重新考察上述清單 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ū)懭氲膯卫?
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)存的指針存儲(chǔ)到
eax
中。下一行代碼,
C3
,獲取
eax
中的指針并將其存儲(chǔ)回內(nèi)存位置為
049388C8
的實(shí)例引用。結(jié)果是,
instance
現(xiàn)在為非
null
并引用一個(gè)有效的
Singleton
對象。然而,此對象的構(gòu)造函數(shù)尚未運(yùn)行,這恰是破壞雙重檢查鎖定的情況。然后,在
C8
行處,
instance
指針被解除引用并存儲(chǔ)到
ecx
。
CA
和
D0
行表示內(nèi)聯(lián)的構(gòu)造函數(shù),該構(gòu)造函數(shù)將值
true
和
5
存儲(chǔ)到
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)該意識(shí)到事情變得有點(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
。
參考資料
關(guān)于作者
|
|
|
Peter Haggar 是 IBM 在北卡羅來納州的 Research Triangle Park 的一名高級軟件工程師,他還是
Practical Java Programming Language Guide
(Addison-Wesley 出版)一書的作者。此外,他還發(fā)表了很多篇關(guān)于 Java 編程的文章。他有著廣泛的編程經(jīng)驗(yàn),曾致力于開發(fā)工具、類庫和操作系統(tǒng)相關(guān)的工作。Peter 在 IBM 致力于研究新興 Internet 技術(shù),目前主要從事高性能 Web 服務(wù)方面的工作。Peter 經(jīng)常在很多行業(yè)會(huì)議上作為技術(shù)發(fā)言人就 Java 技術(shù)發(fā)表言論。他已經(jīng)為 IBM 工作了 14 年多,并獲得了 Clarkson University 的計(jì)算機(jī)科學(xué)學(xué)士學(xué)位。您可以通過
haggar@us.ibm.com
與他聯(lián)系。
|
|