Sealyu

          --- 博客已遷移至: http://www.sealyu.com/blog

            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            618 隨筆 :: 87 文章 :: 225 評(píng)論 :: 0 Trackbacks

          在 水木上看到一篇分析DCL(雙檢測(cè)鎖定-Double Checked Lock)安全性的文章。記得以前也討論過(guò)這個(gè)問(wèn)題,但是為什么DCL也存在隱患,至今沒(méi)弄明白,這篇分析做了比較詳盡的解釋?zhuān)爬ㄆ饋?lái)原因有兩點(diǎn):一是 因?yàn)榫幾g器或者處理器并不是嚴(yán)格按照程序順序進(jìn)行指令調(diào)度。二是java中同步機(jī)制的存在。

          原文來(lái)自:

          發(fā)信人: wyxzellux (I still believe...), 信區(qū): Java
          標(biāo)  題: Singleton模式與雙檢測(cè)鎖定(DCL)
          發(fā)信站: 水木社區(qū) (Mon Apr  7 23:42:14 2008), 站內(nèi)

          看OOP教材時(shí),提到了一個(gè)雙檢測(cè)鎖定(Double-Checked Lock, DCL)的問(wèn)題,但是書(shū)上沒(méi)有多介紹,只是說(shuō)這是一個(gè)和底層內(nèi)存機(jī)制有關(guān)的漏洞。查閱了下相關(guān)資料,對(duì)這個(gè)問(wèn)題大致有了點(diǎn)了解。
          從頭開(kāi)始說(shuō)吧。
          在多線程的情況下Singleton模式會(huì)遇到不少問(wèn)題,一個(gè)簡(jiǎn)單的例子

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

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

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

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

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

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

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

              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上對(duì)應(yīng)文章的標(biāo)題來(lái)評(píng)論這種做法就是smart, but broken。來(lái)看原因:

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

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

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

          好了,回過(guò)頭來(lái)看DCL問(wèn)題??雌饋?lái)訪問(wèn)一個(gè)未同步的instance字段不會(huì)產(chǎn)生什么問(wèn)題,我們?cè)俅蝸?lái)假設(shè)一個(gè)場(chǎng)景:

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

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

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

          錯(cuò)誤發(fā)生的一種情形就是這樣,關(guān)于更詳細(xì)的編譯器指令調(diào)度導(dǎo)致的問(wèn)題,可以參看這個(gè)網(wǎng)頁(yè) [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      ; 接下來(lái)四行是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對(duì)象的初始化,退出了同步塊,并同步了和本地內(nèi)存和主存。線程二來(lái)了,看到一個(gè)非空的引用,拿走。注 意線程二沒(méi)有執(zhí)行一個(gè)Read Barrier,因?yàn)樗揪蜎](méi)進(jìn)后面的同步塊。所以很有可能此時(shí)它看到的數(shù)據(jù)是陳舊的。

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

          [5]中還說(shuō)明了即使把instance字段聲明為volatile還是無(wú)法避免錯(cuò)誤的原因。

          由此可見(jiàn),安全的Singleton的構(gòu)造一般只有兩種方法,一是在類(lèi)載入時(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

          posted on 2009-12-09 17:01 seal 閱讀(629) 評(píng)論(1)  編輯  收藏 所屬分類(lèi): Java基礎(chǔ) 、設(shè)計(jì)模式

          評(píng)論

          # re: DCL(雙檢測(cè)鎖定-Double Checked Lock)安全性分析(轉(zhuǎn)) 2010-02-18 19:36 雨奏
          對(duì)volatile關(guān)鍵字做點(diǎn)說(shuō)明:

          參考資料[5]寫(xiě)于2002年,那時(shí)JDK 5還沒(méi)出來(lái);在參考資料[3]中提到,JDK 5對(duì)volatile進(jìn)行了擴(kuò)展,DCL配合volatile是可行的,不會(huì)再受亂序執(zhí)行的影響

          此外參考資料[2]中還提到一種安全的作法,就是DCL配合ThreadLocal;不過(guò)在某些情況下,特別是早期的JDK,使用這種作法的性能還不如不用DCL  回復(fù)  更多評(píng)論
            

          主站蜘蛛池模板: 江城| 阳原县| 东山县| 宝鸡市| 博湖县| 廊坊市| 和静县| 胶南市| 绵阳市| 布拖县| 收藏| 健康| 开远市| 芒康县| 射洪县| 长沙市| 宜昌市| 山东省| 兴和县| 措美县| 高淳县| 读书| 大埔县| 莱州市| 景谷| 曲水县| 胶南市| 定结县| 大港区| 济南市| 广元市| 柯坪县| 塔河县| 峨眉山市| 密山市| 贵港市| 鹤庆县| 乐东| 新河县| 通河县| 准格尔旗|