走自己的路

          路漫漫其修遠兮,吾將上下而求索

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

          在這本書中文版的第219頁有個例子,講lazy load時用到double checkdouble check比直接用同步的好處是,當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
           

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

           

          個人一開始認為正確的寫法,應該是這樣的

           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局部變量來排除返回實例未初始化完全的情況。因為每次判斷的都是局部變量,每個線程都會有一個自己的tempInstance,這樣就保證每個線程的tempInstance要么是初始化完全的要么就是未初始化的,不會出現中間的情況。要注意的是SingletonNew(1)處是不能去掉的,比如線程構造了一個實例,線程2此時等待在那里,線程2得到鎖,判斷tempInstance == null結果是true,又初始化了一次,這就不是單例了。(2)處的賦值順序也是不能顛倒的,如果顛倒就會出現和Singleton類一樣的情形。


          請大家詳細討論,詳細解釋一下。

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

           

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

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

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

          參考文章: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是復制的引用而已,前者沒有完全初始化后者也肯定是一樣的。我完全沒看出來多賦值一次有什么好處。
          請設計一個試驗,謝謝!
            回復  更多評論
            

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

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

          就算“因為編譯器會在Singleton的構造方法被調用之前隨意給INSTANCE先付一個值”成立了,代碼2不見得就解決了問題  回復  更多評論
            

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

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

          # re: 《Head First Design Pattern 單例模式》中的一個錯誤 2008-07-24 08:13 ldd600
          @朱遠翔-Apusic技術顧問
          謝謝回復。
          因為采用了double check,延遲了同步。所以還是存在線程切換的問題。  回復  更多評論
            

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          # 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通過正確的實現volatile語義能保證對聲明為volatile的變量的讀和寫不會被后續的讀和寫所重排序,因而解決了這個問題。  回復  更多評論
            

          # 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,學習了。  回復  更多評論
            

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

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


          Jvm編譯器會對生成的代碼進行優化,重新排序,甚至移除它認為不必要的代碼,volatile變量之間也是沒有順序保證的。然而jvm保證了classloader load字節碼和靜態變量初始化的同步性,所有把singleton設置為靜態變量是沒有問題的。JMM保證了單線程執行的效果和程序的順序是相同的。JVM對代碼的重新排序和優化是對于程序不可見的,所以在例子2中我不應該假設執行的順序。在讀volatile變量之前,寫行為確保執行完畢,并且更新的值會從線程工作內存(CPU緩存,寄存器)刷新到主內存中,JMM禁止volatile讀入寄存器,其他線程讀取時也會重新load到工作內存中,保證了一致性和可見性,避免讀取臟數據。以前一直以為volatile涉及的只是變量可見性問題,或者說對可見性的適用范圍沒有很好的理解,并不涉及JMM順序性和原子性問題。新的JMM對它進行了擴展,它對volatile變量的重新排序也做了限制。在舊的內存模型當中,volatile變量的多次訪問之間是不能重新排序的,但是它們能在和對非volatile變量訪問代碼之間進行重新排序,新的內存模型不同的是,volatile訪問行為在和非volatile變量的訪問行為的代碼之間重新排序加了一些限制。對volatile的寫行為就和synchronize方法或block釋放監視器(鎖)的效果是一樣的,對volatile字段的讀操作和監視器(鎖)的申請效果也是一樣的。新的模型在volatile字段訪問上做了一些嚴格的限制,只對當前線程可見的變量寫入到volatile共享變量f后,當其他線程讀取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.
          }
          }
          }
          假設當前一個線程正在調用writer方法,其他線程正在調用reader方法,writer方法中對v的寫行為將對x的寫行為釋放到了內存中,v變量的讀取,又重新從內存中獲取了新值。因此,如果讀方法看到了v的值被設為true,也保證了它在這之前就可以看到x的新值42,但這在舊的內存模型中是不保證的。如果v不是volatile的,編譯器可能就會對writer和reader中的代碼進行重新排序,reader方法的訪問有可能得到的x就是0. 可見在新的JMM中,volatile的語義得到了很好的加強,每次對volatile字段的讀和寫可看作是都是半同步。這種順序性(happen-before關系)是針對同一個volatile字段而言的,對不同volatile字段的讀取還是沒有這種順序保證的。在新的JMM下,用volatile就可以解決問題,線程1實例的初始化和線程2的讀取volatile變量就存在一個happen-before關系。
            回復  更多評論
            

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

          <<java 與模式>>  回復  更多評論
            

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

          主站蜘蛛池模板: 通海县| 临漳县| 三明市| 樟树市| 库尔勒市| 修水县| 洞口县| 碌曲县| 延庆县| 琼中| 南澳县| 同江市| 天津市| 金沙县| 营口市| 富源县| 泰顺县| 阳西县| 朝阳市| 扶沟县| 龙胜| 武夷山市| 类乌齐县| 林甸县| 西畴县| 华池县| 肥东县| 肇庆市| 柞水县| 乳源| 新疆| 始兴县| 永福县| 宣汉县| 平利县| 凤凰县| 正镶白旗| 永清县| 榆中县| 阳信县| 民权县|