kapok

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

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

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

            什么是不完全的單例類

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

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

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

          private static LazySingleton 
          m_instance = null; 
          /** 
          * 公開的構(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; 
          }


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

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

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

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

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

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

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

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

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

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

            相關(guān)模式

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

            多例(Multiton)模式

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



            關(guān)于多例模式,請見《Java與模式》一書中的“專題:多例(Multiton)模式與多語言支持”一章。

            簡單工廠(Simple Factory)模式

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



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

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

            問答題

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

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

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

            4. 請問java.lang.Math 類和java.lang.StrictMath 類是否是單例模式?

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


            問答題答案

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

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

            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. 見上題答案。

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

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



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

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

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

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

            什么是雙重檢查成例

            為了解釋什么是雙重檢查成例,請首先看看下面沒有使用任何線程安全考慮的錯誤例子。

            從單線程的程序談起

            首先考慮一個單線程的版本。

            代碼清單13:沒有使用任何線程安全措施的一個例子

          // Single threaded version
          class Foo
          {
           private Helper helper = null;
           public Helper getHelper()
           {
            if (helper == null)
            {
             helper = new Helper();
            }
            return helper;
           }
           // other functions and members...
          }
            這是一個錯誤的例子,詳情請見下面的說明。

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

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

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

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

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

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

            這時,線程A 和B 各自擁有一個獨(dú)立的Helper 對象,而這是錯誤的。

            線程安全的版本

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

            代碼清單14:這是一個正確的答案

          // 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...
          }
            顯然,由于整個靜態(tài)工廠方法都是同步化的,因此,不會有兩個線程同時進(jìn)入這個方法。因此,當(dāng)線程A 和B 作為第一批調(diào)用者同時或幾乎同時調(diào)用此方法時:

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

            (2) 對線程A 來說,helper 變量的值是null ,因此helper = new Helper(); 語句會被執(zhí)行。

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

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

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

            畫蛇添足的“雙重檢查”

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

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

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

            這是一個錯誤的例子,詳情請見下面的解釋。

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

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

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

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

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

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

            到此為止,線程A 和線程B 得到了同一個Helper 對象。可以看到,在上面的方法

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

            代碼清單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;
          }
          }

             這是一個錯誤的例子,請見下面的解釋。

            第一次接觸到這個技巧的讀者必定會有很多問題,諸如第一次檢查或者第二次檢查可不可以省掉等。回答是:按照多線程的原理和雙重檢查成例的預(yù)想方案,它們是不可以省掉的。本節(jié)不打算講解的原因在于雙重檢查成例在Java 編譯器中根本不能成立。

            雙重檢查成例對Java 語言編譯器不成立

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

            文獻(xiàn)[BLOCH01] 指出:一般而言,雙重檢查成立對Java 語言來說是不成立的。

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

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

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

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


          posted on 2005-04-18 14:41 笨笨 閱讀(349) 評論(0)  編輯  收藏 所屬分類: J2EEALLJ2SE
          主站蜘蛛池模板: 清流县| 乐平市| 九寨沟县| 昌黎县| 巴林左旗| 武隆县| 宁波市| 铜梁县| 林芝县| 东安县| 梅州市| 沾化县| 绥德县| 阳江市| 西和县| 江陵县| 响水县| 宜兰县| 白朗县| 钟祥市| 曲阳县| 枣强县| 夏邑县| 肃宁县| 蒙山县| 镶黄旗| 广东省| 红河县| 徐州市| 周口市| 琼结县| 和龙市| 冕宁县| 宁强县| 平泉县| 宿州市| 纳雍县| 潞西市| 罗定市| 连城县| 凌海市|