走自己的路

          路漫漫其修遠(yuǎn)兮,吾將上下而求索

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            50 隨筆 :: 4 文章 :: 118 評論 :: 0 Trackbacks
           

          在這本書中文版的第219頁有個例子,講lazy load時用到double checkdouble check比直接用同步的好處是,當(dāng)Singleton初始化后,就不會有額外的同步操作。它的例子是

           1public class Singleton {
           2    private volatile static Singleton INSTANCE;
           3    
           4    private Singleton() {
           5        
           6    }

           7    
           8    public static Singleton getInstance() {
           9        if(INSTANCE == null{
          10            synchronized (Singleton.class{
          11                if(INSTANCE == null{
          12                    INSTANCE = new Singleton();
          13                }

          14            }

          15        }

          16        return INSTANCE;
          17    }

          18}

          19
           

                  不幸的是,雙重檢查不會保證正常工作,因?yàn)榫幾g器會在Singleton的構(gòu)造方法被調(diào)用之前隨意給INSTANCE先付一個值。如果在INSTANCE引用被賦值之后而被初始化之前線程1被切換,線程2就會被返回一個對未初始化完全的單例類實(shí)例的引用。這樣在程序的其他方法中使用時可能會出現(xiàn)未知的錯誤。

           

          個人一開始認(rèn)為正確的寫法,應(yīng)該是這樣的

           1public class SingletonNew {
           2    private volatile static SingletonNew INSTANCE;
           3    
           4    private SingletonNew() {
           5        
           6    }

           7    
           8    public static SingletonNew getInstance() {
           9        SingletonNew tempInstance = INSTANCE;
          10        if(tempInstance == null{
          11            synchronized (Singleton.class{
          12                tempInstance = INSTANCE;    //(1)
          13                if(tempInstance == null{
          14                    INSTANCE = tempInstance = new SingletonNew(); //(2)
          15                }

          16            }

          17        }

          18        return tempInstance;
          19    }

          20}

          21
           

                

               利用一個tempInstance局部變量來排除返回實(shí)例未初始化完全的情況。因?yàn)槊看闻袛嗟亩际蔷植孔兞浚總€線程都會有一個自己的tempInstance,這樣就保證每個線程的tempInstance要么是初始化完全的要么就是未初始化的,不會出現(xiàn)中間的情況。要注意的是SingletonNew(1)處是不能去掉的,比如線程構(gòu)造了一個實(shí)例,線程2此時等待在那里,線程2得到鎖,判斷tempInstance == null結(jié)果是true,又初始化了一次,這就不是單例了。(2)處的賦值順序也是不能顛倒的,如果顛倒就會出現(xiàn)和Singleton類一樣的情形。


          請大家詳細(xì)討論,詳細(xì)解釋一下。

          -------------------------------------------------------------------------------------------------------------------------------------------------------------------------
          -------------------------------------------------------------------------------------------------------------------------------------------------------------------------
              其實(shí)這兩種寫法在舊的JMM上都是錯誤的,在新的JMM上都是對的,錯誤的原因主要是JMM對代碼的重新排序和優(yōu)化,新的JMM又對volatile的語義進(jìn)行了擴(kuò)展,保證了double-check的正確性。很抱歉一開始讓一些博友產(chǎn)生了困惑,謝謝大家的熱心的討論和回帖,我的主要問題就是出現(xiàn)在對JMM了解不夠深入,只是碎片式的了解一些,沒有很好的了解編譯器對代碼的重新排序和優(yōu)化,當(dāng)然編譯原理課上是學(xué)過的。二又沒有很好的掌握到volatile的新的語義。其實(shí)對一些細(xì)節(jié)了解清楚,可以避免我們的代碼出現(xiàn)一些奇怪的問題,特別是在多線程環(huán)境中。

           

              Jvm編譯器會對生成的代碼進(jìn)行優(yōu)化,重新排序,甚至移除它認(rèn)為不必要的代碼,volatile變量之間也是沒有順序保證的。然而jvm保證了classloader load字節(jié)碼和靜態(tài)變量初始化的同步性,所有把singleton設(shè)置為靜態(tài)變量是沒有問題的。JMM保證了單線程執(zhí)行的效果和程序的順序是相同的。JVM對代碼的重新排序和優(yōu)化是對于程序不可見的,所以在例子2中我不應(yīng)該假設(shè)執(zhí)行的順序。在讀volatile變量之前,寫行為確保執(zhí)行完畢,并且更新的值會從線程工作內(nèi)存(CPU緩存,寄存器)刷新到主內(nèi)存中,JMM禁止volatile讀入寄存器,其他線程讀取時也會重新load到工作內(nèi)存中,保證了一致性和可見性,避免讀取臟數(shù)據(jù)。以前一直以為volatile涉及的只是變量可見性問題,或者說對可見性的適用范圍沒有很好的理解,并不涉及JMM順序性和原子性問題。新的JMM對它進(jìn)行了擴(kuò)展,它對volatile變量的重新排序也做了限制。在舊的內(nèi)存模型當(dāng)中,volatile變量的多次訪問之間是不能重新排序的,但是它們能在和對非volatile變量訪問代碼之間進(jìn)行重新排序,新的內(nèi)存模型不同的是,volatile訪問行為在和非volatile變量的訪問行為的代碼之間重新排序加了一些限制。對volatile的寫行為就和synchronize方法或block釋放監(jiān)視器(鎖)的效果是一樣的,對volatile字段的讀操作和監(jiān)視器(鎖)的申請效果也是一樣的。新的模型在volatile字段訪問上做了一些嚴(yán)格的限制,只對當(dāng)前線程可見的變量寫入到volatile共享變量f后,當(dāng)其他線程讀取f后就是可見的。

          下面這個簡單的例子:

          class VolatileExample {
           int x = 0;
           volatile boolean v = false;
           public void writer() {
              x = 42;
              v = true;
           }
           
           public void reader() {
              if (v == true) {
                //uses x - guaranteed to see 42.
              }
           }
          }

          假設(shè)當(dāng)前一個線程正在調(diào)用writer方法,其他線程正在調(diào)用reader方法,writer方法中對v的寫行為將對x的寫行為釋放到了內(nèi)存中,v變量的讀取,又重新從內(nèi)存中獲取了新值。因此,如果讀方法看到了v的值被設(shè)為true,也保證了它在這之前就可以看到x的新值42,但這在舊的內(nèi)存模型中是不保證的。如果v不是volatile的,編譯器可能就會對writerreader中的代碼進(jìn)行重新排序,reader方法的訪問有可能得到的x就是0. 可見在新的JMM中,volatile的語義得到了很好的加強(qiáng),每次對volatile字段的讀和寫可看作是都是半同步。這種順序性(happen-before關(guān)系)是針對同一個volatile字段而言的,對不同volatile字段的讀取還是沒有這種順序保證的。在新的JMM下,用volatile就可以解決問題,線程1實(shí)例的初始化和線程2的讀取volatile變量就存在一個happen-before關(guān)系。

          JMM對順序性只是提出了一些規(guī)則,具體如何重新排序還是不得而知。

          參考文章:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#reordering
                    《JAVA Language Specification》 17.4



          posted on 2008-07-23 19:51 叱咤紅人 閱讀(2712) 評論(22)  編輯  收藏 所屬分類: Design and Analysis Pattern J2SE and JVM

          評論

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-23 21:20 路過
          即使第一種寫法有問題,你怎么能證明第二種寫法就是對的呢?
          照你的說法INSTANCE是一個沒有完全初始化的對象,那么tempInstance是復(fù)制的引用而已,前者沒有完全初始化后者也肯定是一樣的。我完全沒看出來多賦值一次有什么好處。
          請?jiān)O(shè)計(jì)一個試驗(yàn),謝謝!
            回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-23 23:10 Jarod
          博主是在亂說

          private volatile static SingletonNew INSTANCE;
          static {
          System.out.println(INSTANCE); //null
          }

          就算“因?yàn)榫幾g器會在Singleton的構(gòu)造方法被調(diào)用之前隨意給INSTANCE先付一個值”成立了,代碼2不見得就解決了問題  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤[未登錄] 2008-07-24 06:29 叱咤紅人
          @Jarod
          謝謝回復(fù).
          INSTANCE = new Singleton();我的理解是調(diào)用了構(gòu)造函數(shù),在構(gòu)造之前會先生成一個臨時的值,引用指向一個臨時的地方,具體以前在那里看到的也不太記得了.所以第一種方法線程1進(jìn)入構(gòu)造函數(shù)后,線程2會得到一個不是null的臨時值,所以會得到一個未初始化完全的對象.第二種方法,對全局靜態(tài)變量INSTANCE,沒有用它來作為double check的條件,而是使用了tempInstance局部變量,每個線程都會生成一個自己的tempInstance   回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 07:48 朱遠(yuǎn)翔-Apusic技術(shù)顧問
          @叱咤紅人
          在進(jìn)入初始化之前使用的是線程同步,那么就不存在線程切換的問題呀?   回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 08:13 ldd600
          @朱遠(yuǎn)翔-Apusic技術(shù)顧問
          謝謝回復(fù)。
          因?yàn)椴捎昧薲ouble check,延遲了同步。所以還是存在線程切換的問題。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 08:46 5452
          double check這個東西,現(xiàn)在說不清楚,這種方法沒有辦法確定就是單例。
          JVM建立對象的過程是這樣的:1、先分配一塊內(nèi)存,2、然后把內(nèi)存地址賦值給對象的引用,3、然后調(diào)用類的構(gòu)造函數(shù),生成對象。
          如果一個線程執(zhí)行到第二步的時候,另外一個線程進(jìn)入這個方法,這個時候INSTANCE已經(jīng)不是空的了,但是實(shí)際上還沒有初始化,這樣的話,一定會出問題的~
            回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 09:24 路過
          樓主所提出的問題我可以理解,可是無法理解
          “ if(INSTANCE == null) {”

          “ SingletonNew tempInstance = INSTANCE;
          if(tempInstance == null) {”
          這兩句會得到不同的判斷。
          如果INSTANCE沒有完全初始話,tempInstance也肯定是一樣啊。雖然“每個線程都會生成一個自己的tempInstance”,其實(shí)這些tempInstance和INSTANCE沒有區(qū)別,它們是不同的引用,但是指向同一個對象。
            回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 09:29 ldd600
          @路過
          謝謝回復(fù)
          每個線程生成自己的tempInstance是指這句
          INSTANCE = tempInstance = new SingletonNew(); //(2)
          這句保證了INSTANCE的構(gòu)造的完全性。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 09:38 yswift
          JAVA不支持double check,不管怎么修改,只要用到double check都是錯的,在C++中,書中的例子是完全可以正常工作的。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 10:07 路人
          好像都沒說道正點(diǎn)上,注意volatile關(guān)鍵字。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 10:29 白色天堂
          這段代碼在jdk1.5之后完全沒有問題。之前的版本可能出問題。

          你也沒有理解出錯的原因,所作的改動完全是畫蛇添足。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 11:06 dennis
          無語了,沒看到volatile關(guān)鍵字嗎?  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 11:07 dennis
          @白色天堂
          也不能說完全沒問題,有的jvm實(shí)現(xiàn)在volatile的語義上還是有問題的,只能說在sun jdk1.5及以后版本是沒有問題的。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-24 12:38 路過
          麻煩樓上的講一下為什么
          INSTANCE = tempInstance = new SingletonNew(); //(2)
          這句保證了INSTANCE的構(gòu)造的完全性。
          謝謝。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-24 12:42 路過
          volatile在這段程序里起了什么作用呢?
          樓主說的是得到了一個引用但是引用指向的對象是沒有完全初始化的,又不是說對象已經(jīng)初始化了還是有程序得到了null的引用。
          麻煩樓上的解釋一下,謝謝。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-24 13:09 zhuxing
          @yswift

          yswift同志說的一針見血!  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-25 09:32 dennis
          http://www.ibm.com/developerworks/java/library/j-dcl.html?loc=j

          看看這篇文章,俺就不多說了。原因就在于JMM模型的out-of-order writes問題。jdk5通過正確的實(shí)現(xiàn)volatile語義能保證對聲明為volatile的變量的讀和寫不會被后續(xù)的讀和寫所重排序,因而解決了這個問題。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-25 11:09 路過
          The best solution to this problem is to accept synchronization or use a static field.
          多謝dennis,學(xué)習(xí)了。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-26 21:44 叱咤紅人
          謝謝大家尤其是dennis的熱情討論和回復(fù),其實(shí)這兩種寫法在舊的JMM上都是錯誤的,在新的JMM上都是對的,我主要還是沒有對JMM有更深入的理解,抱歉,繼續(xù)努力好好工作,好好學(xué)習(xí),大家分享。  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-26 21:46 叱咤紅人
          其實(shí)這兩種寫法在舊的JMM上都是錯誤的,在新的JMM上都是對的,錯誤的原因主要是JMM對代碼的重新排序和優(yōu)化,新的JMM又對volatile的語義進(jìn)行了擴(kuò)展,保證了double-check的正確性。很抱歉一開始讓一些博友產(chǎn)生了困惑,謝謝大家的熱心的討論和回帖,我的主要問題就是出現(xiàn)在對JMM了解不夠深入,只是碎片式的了解一些,沒有很好的了解編譯器對代碼的重新排序和優(yōu)化,當(dāng)然編譯原理課上是學(xué)過的。二又沒有很好的掌握到volatile的新的語義。其實(shí)對一些細(xì)節(jié)了解清楚,可以避免我們的代碼出現(xiàn)一些奇怪的問題,特別是在多線程環(huán)境中。


          Jvm編譯器會對生成的代碼進(jìn)行優(yōu)化,重新排序,甚至移除它認(rèn)為不必要的代碼,volatile變量之間也是沒有順序保證的。然而jvm保證了classloader load字節(jié)碼和靜態(tài)變量初始化的同步性,所有把singleton設(shè)置為靜態(tài)變量是沒有問題的。JMM保證了單線程執(zhí)行的效果和程序的順序是相同的。JVM對代碼的重新排序和優(yōu)化是對于程序不可見的,所以在例子2中我不應(yīng)該假設(shè)執(zhí)行的順序。在讀volatile變量之前,寫行為確保執(zhí)行完畢,并且更新的值會從線程工作內(nèi)存(CPU緩存,寄存器)刷新到主內(nèi)存中,JMM禁止volatile讀入寄存器,其他線程讀取時也會重新load到工作內(nèi)存中,保證了一致性和可見性,避免讀取臟數(shù)據(jù)。以前一直以為volatile涉及的只是變量可見性問題,或者說對可見性的適用范圍沒有很好的理解,并不涉及JMM順序性和原子性問題。新的JMM對它進(jìn)行了擴(kuò)展,它對volatile變量的重新排序也做了限制。在舊的內(nèi)存模型當(dāng)中,volatile變量的多次訪問之間是不能重新排序的,但是它們能在和對非volatile變量訪問代碼之間進(jìn)行重新排序,新的內(nèi)存模型不同的是,volatile訪問行為在和非volatile變量的訪問行為的代碼之間重新排序加了一些限制。對volatile的寫行為就和synchronize方法或block釋放監(jiān)視器(鎖)的效果是一樣的,對volatile字段的讀操作和監(jiān)視器(鎖)的申請效果也是一樣的。新的模型在volatile字段訪問上做了一些嚴(yán)格的限制,只對當(dāng)前線程可見的變量寫入到volatile共享變量f后,當(dāng)其他線程讀取f后就是可見的。

          下面這個簡單的例子:

          class VolatileExample {
          int x = 0;
          volatile boolean v = false;
          public void writer() {
          x = 42;
          v = true;
          }

          public void reader() {
          if (v == true) {
          //uses x - guaranteed to see 42.
          }
          }
          }
          假設(shè)當(dāng)前一個線程正在調(diào)用writer方法,其他線程正在調(diào)用reader方法,writer方法中對v的寫行為將對x的寫行為釋放到了內(nèi)存中,v變量的讀取,又重新從內(nèi)存中獲取了新值。因此,如果讀方法看到了v的值被設(shè)為true,也保證了它在這之前就可以看到x的新值42,但這在舊的內(nèi)存模型中是不保證的。如果v不是volatile的,編譯器可能就會對writer和reader中的代碼進(jìn)行重新排序,reader方法的訪問有可能得到的x就是0. 可見在新的JMM中,volatile的語義得到了很好的加強(qiáng),每次對volatile字段的讀和寫可看作是都是半同步。這種順序性(happen-before關(guān)系)是針對同一個volatile字段而言的,對不同volatile字段的讀取還是沒有這種順序保證的。在新的JMM下,用volatile就可以解決問題,線程1實(shí)例的初始化和線程2的讀取volatile變量就存在一個happen-before關(guān)系。
            回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎? 2008-07-26 21:53 zxbyh
          不用去研究這個!
          使用餓漢單例模式就可以了.

          <<java 與模式>>  回復(fù)  更多評論
            

          # re: 《Head First Design Pattern 單例模式》中double check有問題嗎?[未登錄] 2008-08-08 13:26 Chris
          不管哪種方法,在多機(jī)的情況下依然還是解決不了單例的問題,現(xiàn)在機(jī)器那么廉價,那點(diǎn)延遲初始化所帶來的效率是微乎其微的,完全不需要。  回復(fù)  更多評論
            

          主站蜘蛛池模板: 新郑市| 黄浦区| 大渡口区| 师宗县| 鄂托克前旗| 贺州市| 离岛区| 安泽县| 扶余县| 鸡泽县| 永宁县| 象州县| 安溪县| 平陆县| 阜新| 宾川县| 汉寿县| 安达市| 荥经县| 林口县| 海晏县| 九江市| 忻城县| 宝坻区| 桐城市| 江北区| 杭锦旗| 凤台县| 鲁山县| 民县| 新邵县| 曲麻莱县| 澜沧| 收藏| 交城县| 区。| 勐海县| 临桂县| 吉首市| 桐乡市| 商都县|