kapok

          垃圾桶,嘿嘿,我藏的這么深你們還能找到啊,真牛!

            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            455 隨筆 :: 0 文章 :: 76 評(píng)論 :: 0 Trackbacks
          http://www.uml.org.cn/j2ee/j2ee122404.htm

          Java模式設(shè)計(jì)之單例模式(四)
          閻宏 作者授權(quán)
            不完全的單例類 

            什么是不完全的單例類

            估計(jì)有些讀者見(jiàn)過(guò)下面這樣的“不完全”的單例類。

            代碼清單10:“不完全”單例類

          package com.javapatterns.singleton.demos; 
          public class LazySingleton 

          private static LazySingleton 
          m_instance = null; 
          /** 
          * 公開(kāi)的構(gòu)造子,外界可以直接實(shí)例化 
          */ 
          public LazySingleton() { } 
          /** 
          * 靜態(tài)工廠方法 
          * @return 返還LazySingleton 類的惟一實(shí)例 
          */ 
          synchronized public static 
          LazySingleton getInstance() 

          if (m_instance == null) 

          m_instance = new LazySingleton(); 

          return m_instance; 
          }


            上面的代碼乍看起來(lái)是一個(gè)“懶漢”式單例類,仔細(xì)一看,發(fā)現(xiàn)有一個(gè)公開(kāi)的構(gòu)造子。由于外界可以使用構(gòu)造子創(chuàng)建出任意多個(gè)此類的實(shí)例,這違背了單例類只能有一個(gè)(或有限個(gè))實(shí)例的特性,因此這個(gè)類不是完全的單例類。這種情況有時(shí)會(huì)出現(xiàn),比如javax.swing.TimerQueue 便是一例,關(guān)于這個(gè)類,請(qǐng)參見(jiàn)《Java與模式》一書(shū)中的“觀察者模式與Swing 定時(shí)器” 一章。

            造成這種情況出現(xiàn)的原因有以下幾種可能:

           ?。?) 初學(xué)者的錯(cuò)誤。許多初學(xué)者沒(méi)有認(rèn)識(shí)到單例類的構(gòu)造子不能是公開(kāi)的,因此犯下這個(gè)錯(cuò)誤。有些初學(xué)Java 語(yǔ)言的學(xué)員甚至不知道一個(gè)Java 類的構(gòu)造子可以不是公開(kāi)的。在 這種情況下,設(shè)計(jì)師可能會(huì)通過(guò)自我約束,也就是說(shuō)不去調(diào)用構(gòu)造子的辦法,將這個(gè)不完全的單例類在使用中作為一個(gè)單例類使用。

            在這種情況下,一個(gè)簡(jiǎn)單的矯正辦法,就是將公開(kāi)的構(gòu)造子改為私有的構(gòu)造子。

           ?。?) 當(dāng)初出于考慮不周,將一個(gè)類設(shè)計(jì)成為單例類,后來(lái)發(fā)現(xiàn)此類應(yīng)當(dāng)有多于一個(gè)的實(shí)例。為了彌補(bǔ)錯(cuò)誤, 干脆將構(gòu)造子改為公開(kāi)的,以便在需要多于一個(gè)的實(shí)例時(shí), 可以隨時(shí)調(diào)用構(gòu)造子創(chuàng)建新的實(shí)例。要糾正這種情況較為困難,必須根據(jù)具體情況做出改進(jìn)的決定。如果一個(gè)類在最初被設(shè)計(jì)成為單例類,但后來(lái)發(fā)現(xiàn)實(shí)際上此類應(yīng)當(dāng)有有限多個(gè)實(shí)例,這時(shí)候應(yīng)當(dāng)考慮是否將單例類改為多例類(Multiton)。

           ?。?)設(shè)計(jì)師的Java 知識(shí)很好,而且也知道單例模式的正確使用方法,但是還是有意使用這種不完全的單例模式,因?yàn)樗庠谑褂靡环N“改良”的單例模式。這時(shí)候, 除去共有的構(gòu)造子不符合單例模式的要求之外,這個(gè)類必須是很好的單例模式。

            默認(rèn)實(shí)例模式

            有些設(shè)計(jì)師將這種不完全的單例模式叫做“默認(rèn)實(shí)例模式”(Default Instance Pattern)。在所謂的“ 默認(rèn)實(shí)例模式”里面, 一個(gè)類提供靜態(tài)的方法,如同單例模式一樣, 同時(shí)又提供一個(gè)公開(kāi)的構(gòu)造子,如同普通的類一樣。

            這樣做的惟一好處是,這種模式允許客戶端選擇如何將類實(shí)例化:創(chuàng)建新的自己獨(dú)有的實(shí)例,或者使用共享的實(shí)例。這樣一來(lái),由于沒(méi)有任何的強(qiáng)制性措施,客戶端的選擇不一定是合理的選擇。其結(jié)果是設(shè)計(jì)師往往不會(huì)花費(fèi)時(shí)間在如何提供最好的選擇上,而是不恰當(dāng)?shù)貙⑦@種選擇交給客戶端的程序員,這樣必然會(huì)導(dǎo)致不理想的設(shè)計(jì)和欠考慮的實(shí)現(xiàn)。

            本文建議讀者不要這樣做。

            相關(guān)模式

            有一些模式可以使用單例模式,如抽象工廠模式可以使用單例模式,將具體工廠類設(shè)計(jì)成單例類;建造模式可以使用單例模式,將具體建造類設(shè)計(jì)成單例類。

            多例(Multiton)模式

            正如同本章所說(shuō)的,單例模式的精神可以推廣到多于一個(gè)實(shí)例的情況。這時(shí)候這種類叫做多例類,這種模式叫做多例模式。單例類(左)和多例類(右)的類圖如下所示。



            關(guān)于多例模式,請(qǐng)見(jiàn)《Java與模式》一書(shū)中的“專題:多例(Multiton)模式與多語(yǔ)言支持”一章。

            簡(jiǎn)單工廠(Simple Factory)模式

            單例模式使用了簡(jiǎn)單工廠模式(又稱為靜態(tài)工廠方法模式)來(lái)提供自己的實(shí)例。在上面ConfigManager 例子的代碼中, 靜態(tài)工廠方法getInstance() 就是靜態(tài)工廠方法。在java.awt.Toolkit 類中,getDefaultToolkit() 方法就是靜態(tài)工廠方法。簡(jiǎn)單工廠模式的簡(jiǎn)略類圖如下所示。



            本章討論了單例模式的結(jié)構(gòu)和實(shí)現(xiàn)方法。

            單例模式是一個(gè)看上去很簡(jiǎn)單的模式,很多設(shè)計(jì)師最先學(xué)會(huì)的往往是單例模式。然而,隨著Java 系統(tǒng)日益變得復(fù)雜化和分散化,單例模式的使用變得比過(guò)去困難。本書(shū)提醒讀者在分散式的Java 系統(tǒng)中使用單例模式時(shí),盡量不要使用有狀態(tài)的。

            問(wèn)答題

            1. 為什么不使用一個(gè)靜態(tài)的“全程”原始變量,而要建一個(gè)類?一個(gè)靜態(tài)的原始變量當(dāng)然只能有一個(gè)值,自然而然不就是“單例”的嗎?

            2. 舉例說(shuō)明如何調(diào)用EagerSingleton 類。

            3. 舉例說(shuō)明如何調(diào)用RegSingleton 類和RegSingletonChild 類。

            4. 請(qǐng)問(wèn)java.lang.Math 類和java.lang.StrictMath 類是否是單例模式?

            5. 我們公司只購(gòu)買了一個(gè)JDBC 驅(qū)動(dòng)軟件的單用戶使用許可,可否使用單例模式管理通過(guò)JDBC 驅(qū)動(dòng)軟件連接的數(shù)據(jù)庫(kù)?


            問(wèn)答題答案

            1. 單例模式可以提供很復(fù)雜的邏輯,而一個(gè)原始變量不能自已初始化,不可能有繼承的關(guān)系,沒(méi)有內(nèi)部結(jié)構(gòu)。因此單例模式有很多優(yōu)越之處。

            在Java 語(yǔ)言里并沒(méi)有真正的“全程”變量,一個(gè)變量必須屬于某一個(gè)類或者某一個(gè)實(shí)例。而在復(fù)雜的程序當(dāng)中,一個(gè)靜態(tài)變量的初始化發(fā)生在哪里常常是一個(gè)不易確定的問(wèn)題。當(dāng)然,使用“全程”原始變量并沒(méi)有什么錯(cuò)誤,就好像選擇使用Fortran 語(yǔ)言而非Java語(yǔ)言編程并不是一種對(duì)錯(cuò)的問(wèn)題一樣。

            2. 幾種單例類的使用方法如下。

            代碼清單11:幾種單例類的使用方法

          public class RegSingletonTest
          {
          public static void main(String[] args)
          {
          //(1) Test eager
          System.out.println( EagerSingleton.getInstance());
          //(2) Test reg
          System.out.println(
          RegSingleton.getInstance(
          "com.javapatterns.singleton.demos.RegSingleton").about());
          System.out.println( RegSingleton.getInstance(null).about() );
          System.out.println(
          RegSingleton.getInstance(
          "com.javapatterns.singleton.demos.RegSingletonChild").about());
          System.out.println( RegSingletonChild.getInstance().about());
          }
          }
            3. 見(jiàn)上題答案。

            4. 它們都不是單例類。原因如下:

            這兩個(gè)類均有一個(gè)私有的構(gòu)造子。但是這僅僅是單例模式的必要條件,而不是充分條件?;仡櫾诒菊麻_(kāi)始提出的單例模式的三個(gè)特性可以看出,無(wú)論是Math 還是StrictMath 都沒(méi)有為外界提供任何自身的實(shí)例。實(shí)際上,這兩個(gè)類都是被設(shè)計(jì)來(lái)提供靜態(tài)工廠方法和常量的,因此從來(lái)就不需要它們的實(shí)例,這才是它們的構(gòu)造子是私有的原因。Math和StrictMath 類的類圖如下所示。



            5. 這樣做是可行的,只是必須注意當(dāng)使用在分散式系統(tǒng)中的時(shí)候,不一定能保證單例類實(shí)例的惟一性。

            附錄:雙重檢查成例的研究

            成例是一種代碼層次上的模式,是在比設(shè)計(jì)模式的層次更具體的層次上的代碼技巧。成例往往與編程語(yǔ)言密切相關(guān)。雙重檢查成例(Double Check Idiom )是從C 語(yǔ)言移植過(guò)來(lái)的一種代碼模式。在C 語(yǔ)言里,雙重檢查成例常常用在多線程環(huán)境中類的晚實(shí)例化(Late Instantiation)里。

            本節(jié)之所以要介紹這個(gè)成例(嚴(yán)格來(lái)講,是介紹為什么這個(gè)成例不成立), 是因?yàn)橛泻芏嗳苏J(rèn)為雙重檢查成例可以使用在“懶漢”單例模式里面。

            什么是雙重檢查成例

            為了解釋什么是雙重檢查成例,請(qǐng)首先看看下面沒(méi)有使用任何線程安全考慮的錯(cuò)誤例子。

            從單線程的程序談起

            首先考慮一個(gè)單線程的版本。

            代碼清單13:沒(méi)有使用任何線程安全措施的一個(gè)例子

          // Single threaded version
          class Foo
          {
           private Helper helper = null;
           public Helper getHelper()
           {
            if (helper == null)
            {
             helper = new Helper();
            }
            return helper;
           }
           // other functions and members...
          }
            這是一個(gè)錯(cuò)誤的例子,詳情請(qǐng)見(jiàn)下面的說(shuō)明。

            寫出這樣的代碼,本意顯然是要保持在整個(gè)JVM 中只有一個(gè)Helper 的實(shí)例;因此,才會(huì)有if (helper == null) 的檢查。非常明顯的是,如果在多線程的環(huán)境中運(yùn)行,上面的代碼會(huì)有兩個(gè)甚至兩個(gè)以上的Helper 對(duì)象被創(chuàng)建出來(lái),從而造成錯(cuò)誤。

            但是,想像一下在多線程環(huán)境中的情形就會(huì)發(fā)現(xiàn),如果有兩個(gè)線程A 和B 幾乎同時(shí)到達(dá)if (helper == null)語(yǔ)句的外面的話,假設(shè)線程A 比線程B 早一點(diǎn)點(diǎn),那么:

            (1)A 會(huì)首先進(jìn)入if (helper == null) 塊的內(nèi)部,并開(kāi)始執(zhí)行new Helper() 語(yǔ)句。此時(shí),helper 變量仍然是null,直到線程A 的new Helper() 語(yǔ)句返回并給helper 變量賦值為止。

            (2) 但是,線程B 并不會(huì)在if (helper == null)語(yǔ)句的外面等待,因?yàn)榇藭r(shí)helper == null 是成立的,它會(huì)馬上進(jìn)入if (helper == null)語(yǔ)句塊的內(nèi)部。這樣,線程B 會(huì)不可避免地執(zhí)行helper = new Helper();語(yǔ)句,從而創(chuàng)建出第二個(gè)實(shí)例來(lái)。

           ?。?)線程A 的helper = new Helper();語(yǔ)句執(zhí)行完畢后,helper 變量得到了真實(shí)的對(duì)象引用,(helper == null)不再為真。第三個(gè)線程不會(huì)再進(jìn)入if (helper == null) 語(yǔ)句塊的內(nèi)部了。

           ?。?)線程B 的helper = new Helper(); 語(yǔ)句也執(zhí)行完畢后,helper 變量的值被覆蓋。但是第一個(gè)Helper 對(duì)象被線程A 引用的事實(shí)不會(huì)改變。

            這時(shí),線程A 和B 各自擁有一個(gè)獨(dú)立的Helper 對(duì)象,而這是錯(cuò)誤的。

            線程安全的版本

            為了克服沒(méi)有線程安全的缺點(diǎn),下面給出一個(gè)線程安全的例子。

            代碼清單14:這是一個(gè)正確的答案

          // Correct multithreaded version
          class Foo
          {
           private Helper helper = null;
           public synchronized Helper getHelper()
           {
            if (helper == null)
            {
             helper = new Helper();
             return helper;
            }
           }
           // other functions and members...
          }
            顯然,由于整個(gè)靜態(tài)工廠方法都是同步化的,因此,不會(huì)有兩個(gè)線程同時(shí)進(jìn)入這個(gè)方法。因此,當(dāng)線程A 和B 作為第一批調(diào)用者同時(shí)或幾乎同時(shí)調(diào)用此方法時(shí):

           ?。?)早到一點(diǎn)的線程A 會(huì)率先進(jìn)入此方法,同時(shí)線程B 會(huì)在方法外部等待。

           ?。?) 對(duì)線程A 來(lái)說(shuō),helper 變量的值是null ,因此helper = new Helper(); 語(yǔ)句會(huì)被執(zhí)行。

           ?。?)線程A 結(jié)束對(duì)方法的執(zhí)行,helper 變量的值不再是null。

           ?。?)線程B 進(jìn)入此方法,helper 變量的值不再是null ,因此helper = new Helper(); 語(yǔ)句不會(huì)被執(zhí)行。線程B 取到的是helper 變量所含有的引用,也就是對(duì)線程A 所創(chuàng)立的Helper 實(shí)例的引用。

            顯然,線程A 和B 持有同一個(gè)Helper 實(shí)例,這是正確的。

            畫蛇添足的“雙重檢查”

            但是,仔細(xì)審察上面的正確答案會(huì)發(fā)現(xiàn),同步化實(shí)際上只在helper 變量第一次被賦值之前才有用。在helper 變量有了值以后,同步化實(shí)際上變成了一個(gè)不必要的瓶頸。如果能有一個(gè)方法去掉這個(gè)小小的額外開(kāi)銷,不是更加完美了嗎?因此,就有了下面這個(gè)設(shè)計(jì)“巧妙”的雙重檢查成例。在讀者向下繼續(xù)讀之前,有必要提醒一句:正如本小節(jié)的標(biāo)題所標(biāo)明的那樣,這是一個(gè)反面教材,因?yàn)殡p重檢查成例在Java 編譯器里無(wú)法實(shí)現(xiàn)。

            代碼清單15:使用雙重檢查成例的懶漢式單例模式

          // Broken multithreaded version
          // "Double-Checked Locking" idiom
          class Foo
          {
           private Helper helper = null;
           public Helper getHelper()
           {
            if (helper == null) //第一次檢查(位置1)
            {
             //這里會(huì)有多于一個(gè)的線程同時(shí)到達(dá) (位置2)
             synchronized(this)
             {
              //這里在每個(gè)時(shí)刻只能有一個(gè)線程 (位置3)
              if (helper == null) //第二次檢查 (位置4)
              {
               helper = new Helper();
              }
             }
            }
            return helper;
           }
           // other functions and members...
          }

            這是一個(gè)錯(cuò)誤的例子,詳情請(qǐng)見(jiàn)下面的解釋。

            對(duì)于初次接觸雙重檢查成例的讀者來(lái)說(shuō),這個(gè)技巧的思路并不明顯易懂。因此,本節(jié)在這里給出一個(gè)詳盡的解釋。同樣,這里假設(shè)線程A 和B 作為第一批調(diào)用者同時(shí)或幾乎同時(shí)調(diào)用靜態(tài)工廠方法。

           ?。?) 因?yàn)榫€程A 和B 是第一批調(diào)用者,因此,當(dāng)它們進(jìn)入此靜態(tài)工廠方法時(shí),helper 變量是null。因此,線程A 和B 會(huì)同時(shí)或幾乎同時(shí)到達(dá)位置1。

           ?。?)假設(shè)線程A 會(huì)首先到達(dá)位置2,并進(jìn)入synchronized(this) 到達(dá)位置3。這時(shí),由于synchronized(this) 的同步化限制,線程B 無(wú)法到達(dá)位置3,而只能在位置2 等候。

           ?。?)線程A 執(zhí)行helper = new Helper() 語(yǔ)句,使得helper 變量得到一個(gè)值,即對(duì)一個(gè)Helper 對(duì)象的引用。此時(shí),線程B 只能繼續(xù)在位置2 等候。

            (4)線程A 退出synchronized(this) ,返回Helper 對(duì)象,退出靜態(tài)工廠方法。

           ?。?)線程B 進(jìn)入synchronized(this) 塊,達(dá)到位置3,進(jìn)而達(dá)到位置4。由于helper 變量已經(jīng)不是null 了,因此線程B 退出synchronized(this),返回helper 所引用的Helper 對(duì)象(也就是線程A 所創(chuàng)建的Helper 對(duì)象),退出靜態(tài)工廠方法。

            到此為止,線程A 和線程B 得到了同一個(gè)Helper 對(duì)象??梢钥吹?,在上面的方法

            getInstance() 中,同步化僅用來(lái)避免多個(gè)線程同時(shí)初始化這個(gè)類,而不是同時(shí)調(diào)用這個(gè)靜態(tài)工廠方法。如果這是正確的,那么使用這一個(gè)成例之后,“ 懶漢式”單例類就可以擺脫掉同步化瓶頸,達(dá)到一個(gè)很妙的境界。

            代碼清單16:使用了雙重檢查成例的懶漢式單例類

          public class LazySingleton
          {
           private static LazySingleton m_instance = null;
           private LazySingleton() { }
           /**
           * 靜態(tài)工廠方法
           */
           public static LazySingleton getInstance()
           {
            if (m_instance == null)
            {
            //More than one threads might be here!!!
            synchronized(LazySingleton.class)
            {
             if (m_instance == null)
             {
              m_instance = new LazySingleton();
             }
            }
           }
           return m_instance;
          }
          }

             這是一個(gè)錯(cuò)誤的例子,請(qǐng)見(jiàn)下面的解釋。

            第一次接觸到這個(gè)技巧的讀者必定會(huì)有很多問(wèn)題,諸如第一次檢查或者第二次檢查可不可以省掉等?;卮鹗牵喊凑斩嗑€程的原理和雙重檢查成例的預(yù)想方案,它們是不可以省掉的。本節(jié)不打算講解的原因在于雙重檢查成例在Java 編譯器中根本不能成立。

            雙重檢查成例對(duì)Java 語(yǔ)言編譯器不成立

            令人吃驚的是,在C 語(yǔ)言里得到普遍應(yīng)用的雙重檢查成例在多數(shù)的Java 語(yǔ)言編譯器里面并不成立[BLOCH01, GOETZ01, DCL01] 。上面使用了雙重檢查成例的“懶漢式”單例類,不能工作的基本原因在于,在Java 編譯器中,LazySingleton 類的初始化與m_instance 變量賦值的順序不可預(yù)料。如果一個(gè)線程在沒(méi)有同步化的條件下讀取m_instance 引用,并調(diào)用這個(gè)對(duì)象的方法的話,可能會(huì)發(fā)現(xiàn)對(duì)象的初始化過(guò)程尚未完成,從而造成崩潰。

            文獻(xiàn)[BLOCH01] 指出:一般而言,雙重檢查成立對(duì)Java 語(yǔ)言來(lái)說(shuō)是不成立的。

            給讀者的一點(diǎn)建議

            有很多非常聰明的人在這個(gè)成例的Java 版本上花費(fèi)了非常多的時(shí)間,到現(xiàn)在為止人們得出的結(jié)論是:一般而言,雙重檢查成例無(wú)法在現(xiàn)有的Java 語(yǔ)言編譯器里工作[BLOCH01, GOETZ01, DCL01] 。

            讀者可能會(huì)問(wèn),是否有可能通過(guò)某種技巧對(duì)上面的雙重檢查的實(shí)現(xiàn)代碼加以修改,從而使某種形式的雙重檢查成例能在Java 編譯器下工作呢?這種可能性當(dāng)然不能排除,但是除非讀者對(duì)此有特別的興趣,建議不要在這上面花費(fèi)太多的時(shí)間。

            在一般情況下使用餓漢式單例模式或者對(duì)整個(gè)靜態(tài)工廠方法同步化的懶漢式單例模式足以解決在實(shí)際設(shè)計(jì)工作中遇到的問(wèn)題。


          posted on 2005-04-18 14:41 笨笨 閱讀(356) 評(píng)論(0)  編輯  收藏 所屬分類: J2EE 、ALL 、J2SE
          主站蜘蛛池模板: 阿勒泰市| 长乐市| 文昌市| 山西省| 鄂伦春自治旗| 宁国市| 缙云县| 乐陵市| 文山县| 沽源县| 和顺县| 柘荣县| 南丹县| 仁寿县| 中宁县| 灌阳县| 金门县| 林甸县| 马边| 马关县| 贞丰县| 冷水江市| 尤溪县| 玛多县| 达孜县| 孝义市| 雷州市| 图们市| 临潭县| 修水县| 凤翔县| 香河县| 清新县| 巴青县| 南涧| 临海市| 德清县| 新安县| 桂平市| 仁化县| 阳信县|