轉(zhuǎn)載自 ---- http://lifethinker.iteye.com/blog/260515
?
????? 編寫Java多線程程序一直以來都是一件十分困難的事,多線程程序的bug很難測試,DCL(Double Check Lock)就是一個典型,因此對多線程安全的理論分析就顯得十分重要,當(dāng)然這決不是說對多線程程序的測試就是不必要的。傳統(tǒng)上,對多線程程序的分析是通過 分析操作之間可能的執(zhí)行先后順序,然而程序執(zhí)行順序十分復(fù)雜,它與硬件系統(tǒng)架構(gòu),編譯器,緩存以及虛擬機(jī)的實現(xiàn)都有著很大的關(guān)系。僅僅為了分析多線程程序 就需要了解這么多底層知識確實不值得,況且當(dāng)年選擇學(xué)Java就是因為不用理會煩人的硬件和操作系統(tǒng),這導(dǎo)致了許多Java程序員不愿也不能從理論上分析 多線程程序的正確性。雖然99%的Java程序員都知道DCL不對,但是如果讓他們回答一些問題,DCL為什么不對?有什么修正方法?這個修正方法是正確 的嗎?如果不正確,為什么不正確?對于此類問題,他們一臉茫然,或者回答也許吧,或者很自信但其實并沒有抓住根本。
?
幸好現(xiàn)在還有另一條路可走,我們只需要利用幾個基本的happen-before規(guī)則就能從理論上分析Java多線程程序的正確性,而且不需要涉及 到硬件和編譯器的知識。接下來的部分,我會首先說明一下happen-before規(guī)則,然后使用happen-before規(guī)則來分析DCL,最后我以 我自己的例子來說明DCL的問題其實很常見,只是因為對DCL的過度關(guān)注反而忽略其問題本身,當(dāng)然其忽略是有原因的,因為很多人并不知道DCL的問題到底 出在哪里。
?
?
Happen-Before規(guī)則
?
我們一般說一個操作happen-before另一個操作,這到底是什么意思呢?當(dāng)說操作A happen-before操作B時,我們其實是在說在發(fā)生操作B之前,操作A對內(nèi)存施加的影響能夠被觀測到。所謂“對內(nèi)存施加的影響”就是指對變量的寫 入,“被觀測到”指當(dāng)讀取這個變量時能夠得到剛才寫入的值(如果中間沒有發(fā)生其它的寫入)。聽起來很繞口?這就對了,請你保持耐心,舉個例子來說明一下。 線程Ⅰ執(zhí)行了操作A:x=3,線程Ⅱ執(zhí)行了操作B:y=x。如果操作Ahappen-before操作B,線程Ⅱ在執(zhí)行操作B之前就確定操作"x=3"被 執(zhí)行了,它能夠確定,是因為如果這兩個操作之間沒有任何對x的寫入的話,它讀取x的值將得到3,這意味著線程Ⅱ執(zhí)行操作B會寫入y的值為3。如果兩個操作 之間還有對x的寫入會怎樣呢?假設(shè)線程Ⅲ在操作A和B之間執(zhí)行了操作C: x=5,并且操作C和操作B之前并沒有happen-before關(guān)系(后面我會說明時間上的先后并不一定導(dǎo)致happen-before關(guān)系)。這時線 程Ⅱ執(zhí)行操作B會講到x的什么值呢?3還是5?答案是兩者皆有可能,這是因為happen-before關(guān)系 保證一定 能夠觀測到前一個操作施加的內(nèi)存影響,只有時間上的先后關(guān)系而并沒有happen-before關(guān)系 可能但并不保證 能觀測前一個操作施加的內(nèi)存影響。如果讀到了值3,我們就說讀到了“ 陳舊 ”的數(shù)據(jù)。正是多種可能性導(dǎo)致了多線程的不確定性和復(fù)雜性,但是要分析多線程的安全性,我們只能分析確定性部分,這就要求找出happen-before關(guān)系,這又得利用happen-before規(guī)則。
?
下面是我列出的三條非常重要的happen-before規(guī)則,利用它們可以確定兩個操作之間是否存在happen-before關(guān)系。
- 同一個線程中,書寫在前面的操作happen-before書寫在后面的操作。這條規(guī)則是說,在 單線程 中操作間happen-before關(guān)系完全是由源代碼的順序決定的,這里的前提“在同一個線程中”是很重要的,這條規(guī)則也稱為 單線程規(guī)則 。這個規(guī)則多少說得有些簡單了,考慮到控制結(jié)構(gòu)和循環(huán)結(jié)構(gòu),書寫在后面的操作可能happen-before書寫在前面的操作,不過我想讀者應(yīng)該明白我的意思。
- 對鎖的unlock操作happen-before后續(xù)的對同一個鎖的lock操作。這里的“后續(xù)”指的是時間上的先后關(guān)系,unlock操作發(fā) 生在退出同步塊之后,lock操作發(fā)生在進(jìn)入同步塊之前。這是條最關(guān)鍵性的規(guī)則,線程安全性主要依賴于這條規(guī)則。但是僅僅是這條規(guī)則仍然不起任何作用,它 必須和下面這條規(guī)則聯(lián)合起來使用才顯得意義重大。這里關(guān)鍵條件是必須對“同一個鎖”的lock和unlock。
- 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。這條規(guī)則也稱為傳遞規(guī)則。
?
現(xiàn)在暫時放下happen-before規(guī)則,先探討一下“一個操作在時間上先于另一個操作發(fā)生”和“一個操作happen-before另一個操 作之間”的關(guān)系。兩者有關(guān)聯(lián)卻并不相同。關(guān)聯(lián)部分在第2條happen-before規(guī)則中已經(jīng)談到了,通常我們得假定一個時間上的先后順序然后據(jù)此得出 happen-before關(guān)系。不同部分體現(xiàn)在,首先, 一個操作在時間上先于另一個操作發(fā)生,并不意味著一個操作happen-before另一個操作 。看下面的例子:
- public ? void ?setX( int ?x)?{??
- ?? this .x?=?x;??????????????? //?(1) ??
- }??
- ??
- public ? int ?getX()?{??
- ?? return ?x;????????????????? //?(2) ??
- }??
假設(shè)線程Ⅰ先執(zhí)行setX方法,接著線程Ⅱ執(zhí)行g(shù)etX方法,在時間上線程Ⅰ的操作A:this.x = x先于線程Ⅱ的操作B:return x。但是操作A卻并不happen-before操作B,讓我們逐條檢查三條happen-before規(guī)則。第1條規(guī)則在這里不適用,因為這時兩個不同 的線程。第2條規(guī)則也不適用,因為這里沒有任何同步塊,也就沒有任何lock和unlock操作。第3條規(guī)則必須基于已經(jīng)存在的happen- before關(guān)系,現(xiàn)在沒有得出任何happen-before關(guān)系,因此第三條規(guī)則對我們也任何幫助。通過檢查這三條規(guī)則,我們就可以得出,操作A和操 作B之間沒有happen-before關(guān)系。這意味著如果線程Ⅰ調(diào)用了setX(3),接著線程Ⅱ調(diào)用了getX(),其返回值可能不是3,盡管兩個操 作之間沒有任何其它操作對x進(jìn)行寫入,它可能返回任何一個曾經(jīng)存在的值或者默認(rèn)值0。“任何曾經(jīng)存在的值”需要做點解釋,假設(shè)在線程Ⅰ調(diào)用setX(3) 之前,還有別的線程或者就是線程Ⅰ還調(diào)用過setX(5), setX(8),那么x的曾經(jīng)可能值為0, 5和8(這里假設(shè)setX是唯一能夠改變x的方法),其中0是整型的默認(rèn)值,用在這個例子中,線程Ⅱ調(diào)用getX()的返回值可能為0, 3, 5和8,至于到底是哪個值是不確定的。
?
現(xiàn)在將兩個方法都設(shè)成同步的,也就是如下:
- public ? synchronized ? void ?setX( int ?x)?{??
- ?? this .x?=?x;??????????????? //?(1) ??
- }??
- ??
- public ? synchronized ? int ?getX()?{??
- ?? return ?x;????????????????? //?(2) ??
- }??
做同樣的假設(shè),線程Ⅰ先執(zhí)行setX方法,接著線程Ⅱ執(zhí)行g(shù)etX方法,這時就可以得出來,線程Ⅰ的操作A happen-before線程Ⅱ的操作B。下面我們來看如何根據(jù)happen-before規(guī)則來得到這個結(jié)論。由于操作A處于同步塊中,操作A之后必 須定要發(fā)生對this鎖的unlock操作,操作B也處于同步塊中,操作B之前必須要發(fā)生對this鎖的lock操作,根據(jù)假設(shè)unlock操作發(fā)生 lock操作之前,根據(jù)第2條happen-before規(guī)則,就得到unlock操作happen-before于lock操作;另外根據(jù)第1條 happen-before規(guī)則(單線程規(guī)則),操作A happen-before于unlock操作,lock操作happen-before于操作B;最后根據(jù)第3條happen-before規(guī)則(傳遞 規(guī)則),A -> unlock, unlock -> lock, lock -> B(這里我用->表示happen-before關(guān)系),有 A -> B,也就是說操作A happen-before操作B。這意味著如果線程Ⅰ調(diào)用了setX(3),緊接著線程Ⅱ調(diào)用了getX(),如果中間再沒有其它線程改變x的值,那么 其返回值必定是3。
?
如果將兩個方法的任何一個synchronized關(guān)鍵字去掉又會怎樣呢?這時能不能得到線程Ⅰ的操作A happen-before線程Ⅱ的操作B呢?答案是得不到。這里因為第二條happen-before規(guī)則的條件已經(jīng)不成立了,這時因為要么只有線程Ⅰ 的unlock操作(如果去掉getX的synchronized),要么只有線程Ⅱ的lock操作(如果去掉setX的synchronized關(guān)鍵 字)。這里也告訴我們一個原則, 必須對同一個變量的 所有 讀寫同步,才能保證不讀取到陳舊的數(shù)據(jù),僅僅同步讀或?qū)懯遣粔虻? 。
?
其次, 一個操作happen-before另一個操作 也并不意味著 一個操作在時間上先于另一個操作發(fā)生 。看下面的例子:
- x?=? 3 ;??????( 1 )??
- y?=? 2 ;??????( 2 )??
同一個線程執(zhí)行上面的兩個操作,操作A:x = 3和操作B:y = 2。根據(jù)單線程規(guī)則,操作A happen-before操作B,但是操作A卻不一定在時間上先于操作B發(fā)生,這是因為編譯器的重新排序等原因,操作B可能在時間上后于操作B發(fā)生。這 個例子也說明了,分析操作上先后順序是多么地不靠譜,它可能完全違反直觀感覺。
?
最后,一個操作和另一個操作必定存在某個順序,要么一個操作或者是先于或者是后于另一個操作,或者與兩個操作同時發(fā)生。同時發(fā)生是完全可能存在的, 特別是在多CPU的情況下。而兩個操作之間卻可能沒有happen-before關(guān)系,也就是說有可能發(fā)生這樣的情況,操作A不happen- before操作B,操作B也不happen-before操作A,用數(shù)學(xué)上的術(shù)語happen-before關(guān)系是個偏序關(guān)系。兩個存在happen- before關(guān)系的操作不可能同時發(fā)生,一個操作A happen-before操作B,它們必定在時間上是完全錯開的,這實際上也是同步的語義之一(獨(dú)占訪問)。
?
在運(yùn)用happen-before規(guī)則分析DCL之前,有必要對“操作”澄清一下,在前面的敘述中我一直將語句是操作的同義詞,這么講是不嚴(yán)格的, 嚴(yán)格上來說這里的操作應(yīng)該是指單個虛擬機(jī)的指令,如moniterenter, moniterexit, add, sub, store, load等。使用語句來代表操作并不影響我們的分析,下面我仍將延續(xù)這一傳統(tǒng),并且將直接用語句來代替操作。唯一需要注意的是單個語句實際上可能由多個指 令組成,比如語句x=i++由兩條指令(inc和store)組成。現(xiàn)在我們已經(jīng)完成了一切理論準(zhǔn)備,你一定等不及要動手開干了(我都寫煩了)。
?
?
利用Happen-Before規(guī)則分析DCL
?
下面是一個典型的使用DCL的例子:
?
- public ? class ?LazySingleton?{??
- ???? private ? int ?someField;??
- ??????
- ???? private ? static ?LazySingleton?instance;??
- ??????
- ???? private ?LazySingleton()?{??
- ???????? this .someField?=? new ?Random().nextInt( 200 )+ 1 ;????????? //?(1) ??
- ????}??
- ??????
- ???? public ? static ?LazySingleton?getInstance()?{??
- ???????? if ?(instance?==? null )?{??????????????????????????????? //?(2) ??
- ???????????? synchronized (LazySingleton. class )?{??????????????? //?(3) ??
- ???????????????? if ?(instance?==? null )?{??????????????????????? //?(4) ??
- ????????????????????instance?=? new ?LazySingleton();??????????? //?(5) ??
- ????????????????}??
- ????????????}??
- ????????}??
- ???????? return ?instance;?????????????????????????????????????? //?(6) ??
- ????}??
- ??????
- ???? public ? int ?getSomeField()?{??
- ???????? return ? this .someField;???????????????????????????????? //?(7) ??
- ????}??
- }??
?
為了分析DCL,我需要預(yù)先陳述上面程序運(yùn)行時幾個事實:
- 語句(5)只會被執(zhí)行一次,也就是LazySingleton只會存在一個實例,這是由于它和語句(4)被放在同步塊中被執(zhí)行的緣故,如果去掉語句(3)處的同步塊,那么這個假設(shè)便不成立了。
- instance只有兩種“曾經(jīng)可能存在”的值,要么為null,也就是初始值,要么為執(zhí)行語句(5)時構(gòu)造的對象引用。這個結(jié)論由事實1很容易推出來。
- getInstance()總是返回非空值,并且每次調(diào)用返回相同的引用。如果getInstance()是初次調(diào)用,它會執(zhí)行語句(5)構(gòu)造一 個LazySingleton實例并返回,如果getInstance()不是初次調(diào)用,如果不能在語句(2)處檢測到非空值,那么必定將在語句(4)處 就能檢測到instance的非空值,因為語句(4)處于同步塊中,對instance的寫入--語句(5)也處于同一個同步塊中。
有讀者可能要問了,既然根據(jù)第3條事實getInstance()總是返回相同的正確的引用,為什么還說DCL有問題呢? 這里的關(guān)鍵是 盡管得到了LazySingleton的正確引用,但是卻有可能訪問到其成員變量 的 不正確值 ,具體來說LazySingleton.getInstance().getSomeField()有可能返回someField的默認(rèn)值0。如果程序行 為正確的話,這應(yīng)當(dāng)是不可能發(fā)生的事,因為在構(gòu)造函數(shù)里設(shè)置的someField的值不可能為0。為也說明這種情況理論上有可能發(fā)生,我們只需要說明語句 (1)和語句(7)并不存在happen-before關(guān)系。
?
假設(shè)線程Ⅰ是初次調(diào)用getInstance()方法,緊接著線程Ⅱ也調(diào)用了getInstance()方法和getSomeField()方法, 我們要說明的是線程Ⅰ的語句(1)并不happen-before線程Ⅱ的語句(7)。線程Ⅱ在執(zhí)行g(shù)etInstance()方法的語句(2)時,由于 對instance的訪問并沒有處于同步塊中,因此線程Ⅱ可能觀察到也可能觀察不到線程Ⅰ在語句(5)時對instance的寫入,也就是說 instance的值可能為空也可能為非空。我們先假設(shè)instance的值非空,也就觀察到了線程Ⅰ對instance的寫入,這時線程Ⅱ就會執(zhí)行語句 (6)直接返回這個instance的值,然后對這個instance調(diào)用getSomeField()方法,該方法也是在沒有任何同步情況被調(diào)用,因此 整個線程Ⅱ的操作都是在沒有同步的情況下調(diào)用 ,這時我們無法利用第1條和第2條happen-before規(guī)則得到線程Ⅰ的操作和線程Ⅱ的操作之間的任何有效的happen-before關(guān)系,這說 明線程Ⅰ的語句(1)和線程Ⅱ的語句(7)之間并不存在happen-before關(guān)系,這就意味著線程Ⅱ在執(zhí)行語句(7)完全有可能觀測不到線程Ⅰ在語 句(1)處對someFiled寫入的值,這就是DCL的問題所在。很荒謬,是吧?DCL原本是為了逃避同步,它達(dá)到了這個目的,也正是因為如此,它最終 受到懲罰,這樣的程序存在嚴(yán)重的bug,雖然這種bug被發(fā)現(xiàn)的概率絕對比中彩票的概率還要低得多,而且是轉(zhuǎn)瞬即逝,更可怕的是,即使發(fā)生了你也不會想到 是DCL所引起的。
?
前面我們說了,線程Ⅱ在執(zhí)行語句(2)時也有可能觀察空值,如果是種情況,那么它需要進(jìn)入同步塊,并執(zhí)行語句(4)。在語句(4)處線程Ⅱ還能夠讀 到instance的空值嗎?不可能。這里因為這時對instance的寫和讀都是發(fā)生在同一個鎖確定的同步塊中,這時讀到的數(shù)據(jù)是最新的數(shù)據(jù)。為也加深 印象,我再用happen-before規(guī)則分析一遍。線程Ⅱ在語句(3)處會執(zhí)行一個lock操作,而線程Ⅰ在語句(5)后會執(zhí)行一個unlock操 作,這兩個操作都是針對同一個鎖--LazySingleton.class,因此根據(jù)第2條happen-before規(guī)則,線程Ⅰ的unlock操作 happen-before線程Ⅱ的lock操作,再利用單線程規(guī)則,線程Ⅰ的語句(5) -> 線程Ⅰ的unlock操作,線程Ⅱ的lock操作 -> 線程Ⅱ的語句(4),再根據(jù)傳遞規(guī)則,就有線程Ⅰ的語句(5) -> 線程Ⅱ的語句(4),也就是說線程Ⅱ在執(zhí)行語句(4)時能夠觀測到線程Ⅰ在語句(5)時對LazySingleton的寫入值。接著對返回的 instance調(diào)用getSomeField()方法時,我們也能得到線程Ⅰ的語句(1) -> 線程Ⅱ的語句(7),這表明這時getSomeField能夠得到正確的值。但是僅僅是這種情況的正確性并不妨礙DCL的不正確性,一個程序的正確性必須 在所有的情況下的行為都是正確的,而不能有時正確,有時不正確。
?
對DCL的分析也告訴我們一條經(jīng)驗原則, 對引用(包括對象引用和數(shù)組引用)的非同步訪問,即使得到該引用的最新值,卻并不能保證也能得到其成員變量(對數(shù)組而言就是每個數(shù)組元素)的最新值。
?
再稍微對DCL探討一下,這個例子中的LazySingleton是一個不變類,它只有g(shù)et方法而沒有set方法。由對DCL的分析我們知道, 即使一個對象是不變的,在不同的線程中它的同一個方法也可能返回不同的值 。之所以會造成這個問題,是因為LazySingleton實例沒有被安全發(fā)布,所謂“被安全的發(fā)布”是指所有的線程應(yīng)該在同步塊中獲得這個實例。這樣我們又得到一個經(jīng)驗原則, 即使對于不可變對象,它也必須被安全的發(fā)布,才能被安全地共享。 所謂“安全的共享”就是說不需要同步也不會遇到數(shù)據(jù)競爭的問題。在Java5或以后,將someField聲明成final的,即使它不被安全的發(fā)布,也能被安全地共享,而在Java1.4或以前則必須被安全地發(fā)布。
?
關(guān)于DCL的修正
?
既然理解了DCL的根本原因,或許我們就可以修正它。
?
既然原因是線程Ⅱ執(zhí)行g(shù)etInstance()可能根本沒有在同步塊中執(zhí)行,那就將整個方法都同步吧。這個毫無疑問是正確的,但是這卻回到最初的 起點(返樸歸真了),也完全違背了DCL的初衷,盡可能少的減少同步。雖然這不能帶任何意義,卻也說明一個道理,最簡單的往往是最好的。
?
如果我們嘗試不改動getInstance()方法,而是在getSomeField()上做文章,那么首先想到的應(yīng)該是將getSomeField設(shè)置成同步,如下所示:
?
- public ? synchronized ? int ?getSomeField()?{??
- ???? return ? this .someField;???????????????????????????????? //?(7) ??
- }??
?
這種修改是不是正確的呢?答案是不正確。這是因為,第2條happen-before規(guī)則的前提條件并不成立。語句(5)所在同步塊和語句(7)所在同步塊并不是使用同一個鎖。像下面這樣修改才是對的:
- public ? int ?getSomeField()?{??
- ???? synchronized (LazySingleton. class )?{??
- ???????? return ? this .someField;??
- ????}??
- }??
但是這樣的修改雖然能保證正確性卻不能保證高性能。因為現(xiàn)在每次讀訪問getSomeField()都要同步,如果使用簡單的方法,將整個 getInstance()同步,只需要在getInstance()時同步一次,之后調(diào)用getSomeField()就不需要同步了。另外 getSomeField()方法也顯得很奇怪,明明是要返回實例變量卻要使用Class鎖。這也再次驗證了一個道理,簡單的才是好的。
?
好了,由于我的想象力有限,我能想到的修正也就僅限于此了,讓我們來看看網(wǎng)上提供的修正吧。
?
首先看Lucas Lee的修正( 這里 是原帖):
- private ? static ?LazySingleton?instance;??
- private ? static ? int ?hasInitialized?=? 0 ;??
- ??????
- public ? static ?LazySingleton?getInstance()?{??
- ???? if ?(hasInitialized?==? 0 )?{?????????????????????????????????????????? //?(4) ??
- ???????? synchronized (LazySingleton. class )?{????????????????????????? //?(5) ??
- ???????????? if ?(instance?==? null )?{????????????????????????????????? //?(6) ??
- ????????????????instance?=? new ?LazySingleton();????????????????????? //?(7) ??
- ????????????????hasInitialized?=? 1 ;??
- ????????????}??
- ????????}??
- ????}??
- ???? return ?instance;???????????????????????????????????????????????? //?(8) ??
- }??
如果你明白我前面所講的,那么很容易看出這里根本就是一個偽修正,線程Ⅱ仍然完全有可能在非同步狀態(tài)下返回instance。Lucas Lee的理由是對int變量的賦值是原子的,但實際上對instance的賦值也是原子的,Java語言規(guī)范規(guī)定對任何引用變量和基本變量的賦值都是原子 的,除了long和double以外。使用hasInitialized==0和instance==null來判斷LazySingleton有沒有初 始化沒有任何區(qū)別。Lucas Lee對 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 中的最后一個例子有些誤解,里面的計算hashCode的例子之所以是正確的,是因為它返回的是int而不是對象的引用,因而不存在訪問到不正確成員變量值的問題。
?
neuzhujf的修正:
- public ? static ?LazySingleton?getInstance()?{??
- ???? if ?(instance?==? null )?{????????????????????????????????????????? //?(4) ??
- ???????? synchronized (LazySingleton. class )?{????????????????????????? //?(5) ??
- ???????????? if ?(instance?==? null )?{????????????????????????????????? //?(6) ??
- ????????????????LazySingleton?localRef?=? new ?LazySingleton();??
- ????????????????instance?=?localRef;???????????????????????? //?(7) ??
- ????????????}??
- ????????}??
- ????}??
- ???? return ?instance;???????????????????????????????????????????????? //?(8) ??
- }??
這里只是引入了一個局部變量,這也容易看出來只是一個偽修正,如果你弄明白了我前面所講的。
?
既然提到DCL,就不得不提到一個經(jīng)典的而且正確的修正。就是使用一個static holder,kilik在回復(fù)中給出了這樣的一個修正。由于這里一種完全不同的思路,與我這里講的內(nèi)容也沒有太大的關(guān)系,暫時略了吧。另外一個修正是使 用是threadlocal,都可以參見 這篇文章 。
?
步入Java5
?
前面所講的都是基于Java1.4及以前的版本,java5對內(nèi)存模型作了重要的改 動,其中最主要的改動就是對volatile和final語義的改變。本文使用的happen-before規(guī)則實際上是從Java5中借鑒而來,然后再 移花接木到Java1.4中,因此也就不得不談下Java5中的多線程了。
?
在java 5中多增加了一條happen-before規(guī)則:
- 對volatile字段的寫操作happen-before后續(xù)的對同一個字段的讀操作。
利用這條規(guī)則我們可以將instance聲明為volatile,即:
- private ? volatile ? static ?LazySingleton?instance;??
?根據(jù)這條規(guī)則,我們可以得到,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據(jù)單線程規(guī)則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和語線程Ⅱ的句(2) -> 語線程Ⅱ的句(7),再根據(jù)傳遞規(guī)則就有線程Ⅰ的語句(1) -> 語線程Ⅱ的句(7),這表示線程Ⅱ能夠觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序能夠得到正確的行為。
?
在java5之前對final字段的同步語義和其它變量沒有什么區(qū)別,在java5中,final變量一旦在構(gòu)造函數(shù)中設(shè)置完成(前提是在構(gòu)造函數(shù) 中沒有泄露this引用),其它線程必定會看到在構(gòu)造函數(shù)中設(shè)置的值。而DCL的問題正好在于看到對象的成員變量的默認(rèn)值,因此我們可以將 LazySingleton的someField變量設(shè)置成final,這樣在java5中就能夠正確運(yùn)行了。
?
?
遭遇同樣錯誤
?
在Java世界里,框架似乎做了很多事情來隱藏多線程,以至于很多程序員認(rèn)為不再需要關(guān)注多線程了。 這實際上是個陷阱,這它只會使我們對多線程程序的bug反應(yīng)遲鈍。大部分程序員(包括我)都不 會特別留意類文檔中的線程不安全警告,自己寫程序時也不會考慮將該類是否線程安全寫入文檔 中。做個測試,你知道java.text.SimpleDateFormat不是線程安全的嗎?如果你不知道,也不要感到奇怪,我也是在《Java Concurrent In Practice 》這書中才看到的。
?
現(xiàn)在我們已經(jīng)明白了DCL中的問題,很多人都只認(rèn)為這只不過是不切實際的理論者整天談?wù)摰脑掝},殊不知這樣的錯誤其實很常見。我就犯過,下面是從我同一個項目中所寫的代碼中摘錄出來的,讀者也不妨拿此來檢驗一下自己,你自己犯過嗎?即使沒有,你會毫不猶豫的這樣寫嗎?
?
第一個例子:
- public ? class ?TableConfig?{??
- ???? //.... ??
- ???? private ?FieldConfig[]?allFields;??
- ??????
- ???? private ? transient ?FieldConfig[]?_editFields;??
- ??
- ???? //.... ??
- ??????
- ???? public ?FieldConfig[]?getEditFields()?{??
- ???????? if ?(_editFields?==? null )?{??
- ????????????List<FieldConfig>?editFields?=? new ?ArrayList<FieldConfig>();??
- ???????????? for ?( int ?i?=? 0 ;?i?<?allFields.length;?i++)?{??
- ???????????????? if ?(allFields[i].editable)?editFields.add(allFields[i]);??
- ????????????}??
- ????????????_editFields?=?editFields.toArray( new ?FieldConfig[editFields.size()]);??
- ????????}??
- ???????? return ?_editFields;??
- ????}??
- }??
這里緩存了TableConfig的_editFields,免得以后再取要重新遍歷allFields。 這里存在和DCL同樣的問題,_editFields數(shù)組的引用可能是正確的值,但是數(shù)組成員卻可能null! 與DCL不同的是 ,由于對_editFields的賦值沒有同步,它可能被賦值多次,但是在這里沒有問題,因為每次賦值雖然其引用值不同,但是其數(shù)組成員是相同的,對于我 的業(yè)務(wù)來說,它們都等價的。由于我的代碼是要用在java1.4中,因此唯一的修復(fù)方法就是將整個方法聲明為同步。
?
第二個例子:
- private ?Map?selectSqls?=? new ?HashMap();??
- ??
- public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)?{??
- ???? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
- ????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
- ????}??
- ????PreparedSql?psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
- ??
- ????List?result?=?executeSql(...);??
- ?????????
- ??????? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
- ???}??
上面的代碼用constructSelectSql()方法來動態(tài)構(gòu)造SQL語句,為了避免構(gòu)造的開銷,將先前構(gòu)造的結(jié)果緩存在 selectSqls這個Map中,下次直接從緩存取就可以了。顯然由于沒有同步,這段代碼會遭遇和DCL同樣的問題,雖然 selectSqls.get(...)可能能夠返回正確的引用,但是卻有可能返回該引用成員變量的非法值。另外selectSqls使用了非同步的 Map,并發(fā)調(diào)用時可能會破壞它的內(nèi)部狀態(tài),這會造成嚴(yán)重的后果,甚至程序崩潰。可能的修復(fù)就是將整個方法聲明為同步:
?
?
- public ? synchronized ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
- ???? //?.... ??
- ???}??
但是這樣馬上會遭遇吞吐量的問題,這里在同步塊執(zhí)行了數(shù)據(jù)庫查詢,執(zhí)行數(shù)據(jù)庫查詢是是個很慢的操作,這會導(dǎo)致其它線程執(zhí)行同樣的操作時造成不必要的等待,因此較好的方法是減少同步塊的作用域,將數(shù)據(jù)庫查詢操作排除在同步塊之外:
- public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
- ????PreparedSql?psql?=? null ;??
- ???? synchronized ( this )?{??
- ???? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
- ????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
- ????}??
- ????psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
- ????}??
- ??
- ????List?result?=?executeSql(...);??
- ??????
- ???? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
- }??
現(xiàn)在情況已經(jīng)改善了很多,畢竟我們將數(shù)據(jù)庫查詢操作拿到同步塊外面來了。但是仔細(xì)觀察會發(fā)現(xiàn)將this作為同步鎖并不是一個好主意,同步塊的目的是 保證從selectSqls這個Map中取到的是一致的對象,因此用selectSqls作為同步鎖會更好,這能夠提高性能。這個類中還存在很多類似的方 法executeUpdate,executeInsert時,它們都有自己的sql緩存,如果它們都采用this作為同步鎖,那么在執(zhí)行 executeSelect方法時需要等待executeUpdate方法,而這種等待原本是不必要的。使用細(xì)粒度的鎖,可以消除這種等待,最后得到修改 后的代碼:
- private ?Map?selectSqls?=?Collections.synchronizedMap( new ?HashMap())??
- ??? public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
- ????PreparedSql?psql?=? null ;??
- ???? synchronized (selectSqls)?{??
- ???????? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
- ????????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
- ????????}??
- ????????psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
- ????}??
- ??
- ????List?result?=?executeSql(...);??
- ?????????
- ??????? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
- ???}??
我對selectSqls使用了同步Map,如果它只被這個方法使用,這就不是必須的。作為一種防范措施,雖然這會稍微降低性能,即便當(dāng)它被其它方 法使用了也能夠保護(hù)它的內(nèi)部結(jié)構(gòu)不被破壞。并且由于Map的內(nèi)部鎖是非競爭性鎖,根據(jù)官方說法,這對性能影響很小,可以忽略不計。這里我有意無意地提到了 編寫高性能的兩個原則, 盡量減少同步塊的作用域,以及使用細(xì)粒度的鎖 ,關(guān)于細(xì)粒度鎖的最經(jīng)典例子莫過于讀寫鎖了。這兩個原則要慎用,除非你能保證你的程序是正確的。
?
?
結(jié)束語
?
在這篇文章中我主要講到happen-before規(guī)則,并運(yùn)用它來分析DCL問題,最后我用例子來說明DCL問題并不只是理論上的討論,在實際程 序中其實很常見。我希望讀者能夠明白用happen-before規(guī)則比使用時間的先后順序來分析線程安全性要有效得多,作為對比,你可以看看這篇 經(jīng)典的文章 中是如何分析DCL的線程安全性的。它是否講明白了呢?如果它講明白了,你是否又能理解?我想答案很可能是否定的,不然的話就不會出現(xiàn)這么多對DCL的誤 解了。當(dāng)然我也并不是說要用happen-before規(guī)則來分析所有程序的線程安全性,如果你試著分析幾個程序就會發(fā)現(xiàn)這是件很困難的事,因為這個規(guī)則 實在是太底層了,要想更高效的分析程序的線程安全性,還得總結(jié)和利用了一些高層的經(jīng)驗規(guī)則。關(guān)于這些經(jīng)驗規(guī)則,我在文中也談到了一些,很零碎也不完全。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
