posts - 403, comments - 310, trackbacks - 0, articles - 7
            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          Singleton模式與雙檢測鎖定(DCL)

          Posted on 2008-04-07 21:58 ZelluX 閱讀(2325) 評論(7)  編輯  收藏 所屬分類: OOP

          轉(zhuǎn)載請注明 作者 ZelluX??? http://www.aygfsteel.com/zellux

          看OOP教材時(shí),提到了一個(gè)雙檢測鎖定(Double-Checked Lock, DCL)的問題,但是書上沒有多介紹,只是說這是一個(gè)和底層內(nèi)存機(jī)制有關(guān)的漏洞。查閱了下相關(guān)資料,對這個(gè)問題大致有了點(diǎn)了解。

          從頭開始說吧。

          在多線程的情況下Singleton模式會遇到不少問題,一個(gè)簡單的例子

          ?? 1:? class Singleton {?????
          ?? 2:????? private static Singleton instance = null;?????
          ?? 3:????????
          ?? 4:????? public static Singleton instance() {?????
          ?? 5:????????? if (instance == null) {?????
          ?? 6:????????????? instance = new Singleton();?????
          ?? 7:????????? }?????
          ?? 8:????????? return instance;????
          ?? 9:????? }????
          ?? 10:? }

          ??
          假設(shè)這樣一個(gè)場景,有兩個(gè)線程調(diào)用Singleton.instance(),首先線程一判斷instance是否等于null,判斷完后一瞬間虛擬機(jī)把線程二調(diào)度為運(yùn)行線程,線程二再次判斷instance是否為null,然后創(chuàng)建一個(gè)Singleton實(shí)例,線程二的時(shí)間片用完后,線程一被喚醒,接下來它執(zhí)行的代碼依然是instance = new Singleton();
          兩次調(diào)用返回了不同的對象,出現(xiàn)問題了。

          最簡單的方法自然是在類被載入時(shí)就初始化這個(gè)對象:private static Singleton instance = new Singleton();

          JLS(Java Language Specification)中規(guī)定了一個(gè)類只會被初始化一次,所以這樣做肯定是沒問題的。

          但是如果要實(shí)現(xiàn)延遲初始化(Lazy initialization),比如這個(gè)實(shí)例初始化時(shí)的參數(shù)要在運(yùn)行期才能確定,應(yīng)該怎么做呢?

          依然有最簡單的方法:使用synchronized關(guān)鍵字修飾初始化方法:

          ??? public synchronized static Singleton instance() {???????
          ??????? if (instance == null) {
          ??????????? instance = new Singleton();
          ??????? }
          ??????? return instance;
          ??? }

          ???
          這里有一個(gè)性能問題:多個(gè)線程同時(shí)訪問這個(gè)方法時(shí),會因?yàn)橥蕉鴮?dǎo)致每次只有一個(gè)線程運(yùn)行,影響程序性能。而事實(shí)上初始化完畢后只需要簡單的返回instance的引用就行了。

          DCL是一個(gè)“看似”有效的解決方法,先把對應(yīng)代碼放上來吧:

          ??? 1 :?? class Singleton {??
          ??? 2 :?????? private?static?Singleton instance = null ;??
          ??? 3 :?????
          ??? 4 :?????? public?static Singleton instance() {??
          ??? 5 :?????????? if?(instance?== null ) {
          ??? 6 :?????????????? synchronized?(this) {??
          ??? 7 :?????????????????? if?(instance?==?null)
          ??? 8 :????????????????????? instance = new?Singleton();
          ??? 9 :????????????? }
          ??? 10 :????????? }
          ??? 11 :??????????return instance;
          ??? 12 :????? }
          ??? 13 :? }

          用JavaWorld上對應(yīng)文章的標(biāo)題來評論這種做法就是smart, but broken。來看原因:

          Java編譯器為了提高程序性能會進(jìn)行指令調(diào)度,CPU在執(zhí)行指令時(shí)同樣出于性能會亂序執(zhí)行(至少現(xiàn)在用的大多數(shù)通用處理器都是out-of-order的),另外cache的存在也會改變數(shù)據(jù)回寫內(nèi)存時(shí)的順序[2]。JMM(Java Memory Model, 見[1])指出所有的這些優(yōu)化都是允許的,只要運(yùn)行結(jié)果和嚴(yán)格按順序執(zhí)行所得的結(jié)果一樣即可。

          Java假設(shè)每個(gè)線程都跑在自己的處理器上,享有自己的內(nèi)存,和共享的主存交互。注意即使在單核上這種模型也是有意義的,考慮到cache和寄存器會保存部分臨時(shí)變量。理論上每個(gè)線程修改自己的內(nèi)存后,必須立即更新對應(yīng)的主存內(nèi)容。但是Java設(shè)計(jì)師們認(rèn)為這種約束會影響程序性能,他們試著創(chuàng)造了一套讓程序跑得更快、但又保證線程之間的交互與預(yù)期一致的內(nèi)存模型。

          synchronized關(guān)鍵字便是其中一把利器。事實(shí)上,synchronized塊的實(shí)現(xiàn)和Linux中的信號量(semaphore)還是有區(qū)別的,前者過程中鎖的獲得和釋放都會都會引發(fā)一次Memory Barrier來強(qiáng)制線程本地內(nèi)存和主存之間的同步。通過這個(gè)機(jī)制,Java中的同步機(jī)制保證了synchronized塊中指令的原子性(atomic)。

          好了,回過頭來看DCL問題。看起來訪問一個(gè)未同步的instance字段不會產(chǎn)生什么問題,我們再次來假設(shè)一個(gè)場景:

          線程一進(jìn)入同步塊,執(zhí)行instance = new Singleton(); 線程二剛開始執(zhí)行g(shù)etResource();

          按照順序的話,接下來應(yīng)該執(zhí)行的步驟是 1) 分配新的Singleton對象的內(nèi)存 2) 調(diào)用Singleton的構(gòu)造器,初始化成員字段 3) instance被賦為指向新的對象的引用。

          前面說過,編譯器或處理器都為了提高性能都有可能進(jìn)行指令的亂序執(zhí)行,線程一的真正執(zhí)行步驟可能是1) 分配內(nèi)存 2) instance指向新對象 3) 初始化新實(shí)例。如果線程二在2完成后3執(zhí)行前被喚醒,它看到了一個(gè)不為null的instance,跳出方法體走了,帶著一個(gè)還沒初始化的Singleton對象。

          錯誤發(fā)生的一種情形就是這樣,關(guān)于更詳細(xì)的編譯器指令調(diào)度導(dǎo)致的問題,可以參看這個(gè)網(wǎng)頁 [4]。

          [3] 中提供了一個(gè)編譯器指令調(diào)度的證據(jù)

          instance = new Singleton(); 這條命令在Symantec JIT中被編譯成

          0206106A?? mov???????? eax,0F97E78h
          0206106F?? call??????? 01F6B210????????????????? ; 分配空間
          02061074?? mov???????? dword ptr [ebp],eax?????? ; EBP中保存了instance的地址

          02061077?? mov???????? ecx,dword ptr [eax]?????? ; 解引用,獲得新的指針地址

          02061079?? mov???????? dword ptr [ecx],100h????? ; 接下來四行是inline后的構(gòu)造器
          0206107F?? mov???????? dword ptr [ecx+4],200h???
          02061086?? mov???????? dword ptr [ecx+8],400h
          0206108D?? mov???????? dword ptr [ecx+0Ch],0F84030h

          可以看到,賦值完成在初始化之前,而這是JLS允許的。
          ?
          另一種情形是,假設(shè)線程一安穩(wěn)地完成Singleton對象的初始化,退出了同步塊,并同步了和本地內(nèi)存和主存。線程二來了,看到一個(gè)非空的引用,拿走。注意線程二沒有執(zhí)行一個(gè)Read Barrier,因?yàn)樗揪蜎]進(jìn)后面的同步塊。所以很有可能此時(shí)它看到的數(shù)據(jù)是陳舊的。

          還有很多人根據(jù)已知的幾種提出了一個(gè)又一個(gè)fix的方法,但最終還是出現(xiàn)了更多的問題。可以參閱[3]中的介紹。

          [5]中還說明了即使把instance字段聲明為volatile還是無法避免錯誤的原因。

          由此可見,安全的Singleton的構(gòu)造一般只有兩種方法,一是在類載入時(shí)就創(chuàng)建該實(shí)例,二是使用性能較差的synchronized方法。

          (by ZelluX? http://www.aygfsteel.com/zellux )

          參考資料:

          [1] Java Language Specification, Second Edition, 第17章介紹了Java中線程和內(nèi)存交互關(guān)系的具體細(xì)節(jié)。
          [2] out-of-order與cache的介紹可以參閱Computer System, A Programmer's Perspective的第四、五章。
          [3] The "Double-Checked Locking is Broken" Declaration, http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
          [4] Synchronization and the Java Memory Model, http://gee.cs.oswego.edu/dl/cpj/jmm.html
          [5] Double-checked locking: Clever, but broken, http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html?page=1
          [6] Holub on Patterns, Learning Design Patterns by Looking at Code

          ?


          評論

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-07 23:10 by 王能
          http://www.bt285.cn 這個(gè)BT網(wǎng)站,與http://yaonba.com.cn NBA中文網(wǎng)框架也是用singleton

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-08 11:38 by dennis
          請參考volatile關(guān)鍵字,在jdk5以上版本,將instance聲明為volatile,DCL是可以的。當(dāng)然,最好的方案還是采用static holder。這個(gè)問題真是討論爛了。

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-08 11:46 by ZelluX
          @dennis
          恩,jdk5已經(jīng)fix這個(gè)bug了。

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-08 23:31 by stanleyxu
          You should use singleton only when it is needed.
          You are right, there are two thread-safe ways to access a singleton object.
          1) Create a static singleton object on initialization. (If your code uses this object very frequently, I recommend you take this method)
          2) Use synchronized or critical section, lock, etc in other languages. (You should avoid use this, unless the object must be created at running time.)

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-09 08:53 by ZelluX
          @stanleyxu
          水木上看到過一個(gè)更好的解決方案,利用jvm的ClassLoader機(jī)制保證。

          public Single{
          static class Holder{
          static Single inst = new Single();
          }
          static public Single getInstance(){
          return Holder.inst;
          }
          }

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-12 13:46 by
          @ZelluX

          嗯,這種方法在 Effective Java 中有過介紹。

          # re: Singleton模式與雙檢測鎖定(DCL)  回復(fù)  更多評論   

          2008-04-16 23:50 by luohandsome
          看上去Java 的同步不比內(nèi)核簡單
          主站蜘蛛池模板: 资源县| 高清| 庆安县| 井研县| 青川县| 平遥县| 东平县| 林口县| 浦城县| 菏泽市| 柘城县| 翼城县| 陇西县| 康乐县| 余干县| 闸北区| 彝良县| 洛川县| 惠州市| 平顶山市| 岳阳县| 江山市| 嘉义县| 通许县| 康保县| 巴东县| 安平县| 宜昌市| 页游| 平度市| 胶南市| 东阿县| 三穗县| 石柱| 吴桥县| 和田县| 旬阳县| 康乐县| 苍溪县| 温州市| 清流县|