<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    隨筆 - 63  文章 - 0  trackbacks - 0
    <2009年4月>
    2930311234
    567891011
    12131415161718
    19202122232425
    262728293012
    3456789

    常用鏈接

    留言簿(2)

    隨筆分類

    隨筆檔案

    搜索

    •  

    最新評論

    閱讀排行榜

    評論排行榜

    單例模式是最簡單的設(shè)計模式之一,但是對于Java的開發(fā)者來說,它卻有很多缺陷。在本月的專欄中,David Geary探討了單例模式以及在面對多線程(multithreading)、類裝載器(classloaders)和序列化(serialization)時如何處理這些缺陷。

    單例模式適合于一個類只有一個實例的情況,比如窗口管理器,打印緩沖池和文件系統(tǒng),它們都是原型的例子。典型的情況是,那些對象的類型被遍及一個軟件系統(tǒng)的不同對象訪問,因此需要一個全局的訪問指針,這便是眾所周知的單例模式的應(yīng)用。當(dāng)然這只有在你確信你不再需要任何多于一個的實例的情況下。
    單例模式的用意在于前一段中所關(guān)心的。通過單例模式你可以:


    確保一個類只有一個實例被建立
    提供了一個對對象的全局訪問指針
    在不影響單例類的客戶端的情況下允許將來有多個實例

    盡管單例設(shè)計模式如在下面的圖中的所顯示的一樣是最簡單的設(shè)計模式,但對于粗心的Java開發(fā)者來說卻呈現(xiàn)出許多缺陷。這篇文章討論了單例模式并揭示了那些缺陷。
    注意:你可以從Resources下載這篇文章的源代碼。

    單例模式

    在《設(shè)計模式》一書中,作者這樣來敘述單例模式的:確保一個類只有一個實例并提供一個對它的全局訪問指針。
    下圖說明了單例模式的類圖。
    (圖1)

    單例模式的類圖

    正如你在上圖中所看到的,這不是單例模式的完整部分。此圖中單例類保持了一個對唯一的單例實例的靜態(tài)引用,并且會從靜態(tài)getInstance()方法中返回對那個實例的引用。
    例1顯示了一個經(jīng)典的單例模式的實現(xiàn)。
    例1.經(jīng)典的單例模式

    Java代碼
    1. public class ClassicSingleton {    
    2.    private static ClassicSingleton instance = null;    
    3.      
    4.    protected ClassicSingleton() {    
    5.       // Exists only to defeat instantiation.    
    6.    }    
    7.    public static ClassicSingleton getInstance() {    
    8.       if(instance == null) {    
    9.          instance = new ClassicSingleton();    
    10.       }    
    11.       return instance;    
    12.    }    
    13. }   


    在例1中的單例模式的實現(xiàn)很容易理解。ClassicSingleton類保持了一個對單獨的單例實例的靜態(tài)引用,并且從靜態(tài)方法getInstance()中返回那個引用。
    關(guān)于ClassicSingleton類,有幾個讓我們感興趣的地方。首先,ClassicSingleton使用了一個眾所周知的懶漢式實例化去創(chuàng)建那個單例類的引用;結(jié)果,這個單例類的實例直到getInstance()方法被第一次調(diào)用時才被創(chuàng)建。這種技巧可以確保單例類的實例只有在需要時才被建立出來。其次,注意ClassicSingleton實現(xiàn)了一個protected的構(gòu)造方法,這樣客戶端不能直接實例化一個ClassicSingleton類的實例。然而,你會驚奇的發(fā)現(xiàn)下面的代碼完全合法:
    Java代碼
    1. public class SingletonInstantiator {     
    2.   public SingletonInstantiator() {     
    3.    ClassicSingleton instance = ClassicSingleton.getInstance();    
    4. ClassicSingleton anotherInstance =    
    5. new ClassicSingleton();    
    6.        ...     
    7.   }     
    8. }   


    前面這個代碼片段為何能在沒有繼承ClassicSingleton并且ClassicSingleton類的構(gòu)造方法是protected的情況下創(chuàng)建其實例?答案是protected的構(gòu)造方法可以被其子類以及在同一個包中的其它類調(diào)用。因為ClassicSingleton和SingletonInstantiator位于相同的包(缺省的包),所以SingletonInstantiator方法能創(chuàng)建ClasicSingleton的實例。
    這種情況下有兩種解決方案:一是你可以使ClassicSingleton的構(gòu)造方法變化私有的(private)這樣只有ClassicSingleton的方法能調(diào)用它;然而這也意味著ClassicSingleton不能有子類。有時這是一種很合意的解決方法,如果確實如此,那聲明你的單例類為final是一個好主意,這樣意圖明確,并且讓編譯器去使用一些性能優(yōu)化選項。另一種解決方法是把你的單例類放到一個外在的包中,以便在其它包中的類(包括缺省的包)無法實例化一個單例類。
    關(guān)于ClassicSingleton的第三點感興趣的地方是,如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的實例。
    第四點,如果ClasicSingleton實現(xiàn)了java.io.Serializable接口,那么這個類的實例就可能被序列化和復(fù)原。不管怎樣,如果你序列化一個單例類的對象,接下來復(fù)原多個那個對象,那你就會有多個單例類的實例。
    最后也許是最重要的一點,就是例1中的ClassicSingleton類不是線程安全的。如果兩個線程,我們稱它們?yōu)榫€程1和線程2,在同一時間調(diào)用ClassicSingleton.getInstance()方法,如果線程1先進入if塊,然后線程2進行控制,那么就會有ClassicSingleton的兩個的實例被創(chuàng)建。

    正如你從前面的討論中所看到的,盡管單例模式是最簡單的設(shè)計模式之一,在Java中實現(xiàn)它也是決非想象的那么簡單。這篇文章接下來會揭示Java規(guī)范對單例模式進行的考慮,但是首先讓我們近水樓臺的看看你如何才能測試你的單例類。

    測試單例模式

    接下來,我使用與log4j相對應(yīng)的JUnit來測試單例類,它會貫穿在這篇文章余下的部分。如果你對JUnit或log4j不很熟悉,請參考相關(guān)資源。

    例2是一個用JUnit測試例1的單例模式的案例:
    例2.一個單例模式的案例

    Java代碼
    1. import org.apache.log4j.Logger;    
    2. import junit.framework.Assert;    
    3. import junit.framework.TestCase;    
    4.      
    5. public class SingletonTest extends TestCase {    
    6.    private ClassicSingleton sone = null, stwo = null;    
    7.    private static Logger logger = Logger.getRootLogger();    
    8.      
    9.    public SingletonTest(String name) {    
    10.       super(name);    
    11.    }    
    12.    public void setUp() {    
    13.       logger.info("getting singleton...");    
    14.       sone = ClassicSingleton.getInstance();    
    15.       logger.info("...got singleton: " + sone);    
    16.      
    17.       logger.info("getting singleton...");    
    18.       stwo = ClassicSingleton.getInstance();    
    19.       logger.info("...got singleton: " + stwo);    
    20.    }    
    21.    public void testUnique() {    
    22.       logger.info("checking singletons for equality");    
    23.       Assert.assertEquals(true, sone == stwo);    
    24.    }    
    25. }   


    例2兩次調(diào)用ClassicSingleton.getInstance(),并且把返回的引用存儲在成員變量中。方法testUnique()會檢查這些引用看它們是否相同。例3是這個測試案例的輸出:
    例3.是這個測試案例的輸出

    Java代碼
    1. Buildfile: build.xml    
    2.      
    3. init:    
    4.      [echo] Build 20030414 (14-04-2003 03:08)    
    5.      
    6. compile:    
    7.      
    8. run-test-text:    
    9.      [java] .INFO main: [b]getting singleton...[/b]    
    10.      [java] INFO main: [b]created singleton:[/b] Singleton@e86f41    
    11.      [java] INFO main: ...got singleton: Singleton@e86f41    
    12.      [java] INFO main: [b]getting singleton...[/b]    
    13.      [java] INFO main: ...got singleton: Singleton@e86f41    
    14.      [java] INFO main: checking singletons for equality    
    15.      
    16.      [java] Time: 0.032    
    17.      
    18.      [java] OK (1 test)  


    正如前面的清單所示,例2的簡單測試順利通過----通過ClassicSingleton.getInstance()獲得的兩個單例類的引用確實相同;然而,你要知道這些引用是在單線程中得到的。下面的部分著重于用多線程測試單例類。


    多線程因素的考慮

    在例1中的ClassicSingleton.getInstance()方法由于下面的代碼而不是線程安全的:
    Java代碼
    1. 1if(instance == null) {    
    2. 2:    instance = new Singleton();    
    3. 3: }   


    如果一個線程在第二行的賦值語句發(fā)生之前切換,那么成員變量instance仍然是null,然后另一個線程可能接下來進入到if塊中。在這種情況下,兩個不同的單例類實例就被創(chuàng)建。不幸的是這種假定很少發(fā)生,這樣這種假定也很難在測試期間出現(xiàn)(譯注:在這可能是作者對很少出現(xiàn)這種情況而導(dǎo)致無法測試從而使人們放松警惕而感到嘆惜)。為了演示這個線程輪換,我得重新實現(xiàn)例1中的那個類。例4就是修訂后的單例類:
    例4.人為安排的方式

    Java代碼
    1. import org.apache.log4j.Logger;    
    2.      
    3. public class Singleton {    
    4.   private static Singleton singleton = null;    
    5.   private static Logger logger = Logger.getRootLogger();    
    6.   private static boolean firstThread = true;    
    7.      
    8.   protected Singleton() {    
    9.     // Exists only to defeat instantiation.    
    10.   }    
    11.   public static Singleton getInstance() {    
    12.      if(singleton == null) {    
    13.         simulateRandomActivity();    
    14.         singleton = new Singleton();    
    15.      }    
    16.      logger.info("created singleton: " + singleton);    
    17.      return singleton;    
    18.   }    
    19.   private static void simulateRandomActivity() {    
    20.      try {    
    21.         if(firstThread) {    
    22.            firstThread = false;    
    23.            logger.info("sleeping...");    
    24.      
    25.            // This nap should give the second thread enough time    
    26.            // to get by the first thread.    
    27.              Thread.currentThread().sleep(50);    
    28.        }    
    29.      }    
    30.      catch(InterruptedException ex) {    
    31.         logger.warn("Sleep interrupted");    
    32.      }    
    33.   }    
    34. }   


    除了在這個清單中的單例類強制使用了一個多線程錯誤處理,例4類似于例1中的單例類。在getInstance()方法第一次被調(diào)用時,調(diào)用這個方法的線程會休眠50毫秒以便另外的線程也有時間調(diào)用getInstance()并創(chuàng)建一個新的單例類實例。當(dāng)休眠的線程覺醒時,它也會創(chuàng)建一個新的單例類實例,這樣我們就有兩個單例類實例。盡管例4是人為如此的,但它卻模擬了第一個線程調(diào)用了getInstance()并在沒有完成時被切換的真實情形。
    例5測試了例4的單例類:
    例5.失敗的測試

    Java代碼
    1. import org.apache.log4j.Logger;    
    2. import junit.framework.Assert;    
    3. import junit.framework.TestCase;    
    4.      
    5. public class SingletonTest extends TestCase {    
    6.    private static Logger logger = Logger.getRootLogger();    
    7.    private static Singleton singleton = null;    
    8.      
    9.    public SingletonTest(String name) {    
    10.       super(name);    
    11.    }    
    12.    public void setUp() {    
    13.       singleton = null;    
    14.    }    
    15.    public void testUnique() throws InterruptedException {    
    16.       // Both threads call Singleton.getInstance().    
    17.       Thread threadOne = new Thread(new SingletonTestRunnable()),    
    18.              threadTwo = new Thread(new SingletonTestRunnable());    
    19.      
    20.       threadOne.start();    
    21.       threadTwo.start();    
    22.      
    23.       threadOne.join();    
    24.       threadTwo.join();    
    25.    }    
    26.    private static class SingletonTestRunnable implements Runnable {    
    27.       public void run() {    
    28.          // Get a reference to the singleton.    
    29.          Singleton s = Singleton.getInstance();    
    30.      
    31.          // Protect singleton member variable from    
    32.          // multithreaded access.    
    33.          synchronized(SingletonTest.class) {    
    34.             if(singleton == null// If local reference is null...    
    35.                singleton = s;     // ...set it to the singleton    
    36.          }    
    37.          // Local reference must be equal to the one and    
    38.          // only instance of Singleton; otherwise, we have two    
    39.                   // Singleton instances.    
    40.          Assert.assertEquals(true, s == singleton);    
    41.       }    
    42.    }    
    43. }   


    例5的測試案例創(chuàng)建兩個線程,然后各自啟動,等待完成。這個案例保持了一個對單例類的靜態(tài)引用,每個線程都會調(diào)用Singleton.getInstance()。如果這個靜態(tài)成員變量沒有被設(shè)置,那么第一個線程就會將它設(shè)為通過調(diào)用getInstance()而得到的引用,然后這個靜態(tài)變量會與一個局部變量比較是否相等。
    在這個測試案例運行時會發(fā)生一系列的事情:第一個線程調(diào)用getInstance(),進入if塊,然后休眠;接著,第二個線程也調(diào)用getInstance()并且創(chuàng)建了一個單例類的實例。第二個線程會設(shè)置這個靜態(tài)成員變量為它所創(chuàng)建的引用。第二個線程檢查這個靜態(tài)成員變量與一個局部備份的相等性。然后測試通過。當(dāng)?shù)谝粋€線程覺醒時,它也會創(chuàng)建一個單例類的實例,并且它不會設(shè)置那個靜態(tài)成員變量(因為第二個線程已經(jīng)設(shè)置過了),所以那個靜態(tài)變量與那個局部變量脫離同步,相等性測試即告失敗。例6列出了例5的輸出:
    例6.例5的輸出

    Java代碼 復(fù)制代碼
    1. Buildfile: build.xml    
    2. init:    
    3.      [echo] Build 20030414 (14-04-2003 03:06)    
    4. compile:    
    5. run-test-text:    
    6. INFO Thread-1: sleeping...    
    7. INFO Thread-2: created singleton: Singleton@7e5cbd    
    8. INFO Thread-1: created singleton: Singleton@704ebb    
    9. junit.framework.AssertionFailedError: expected: but was:    
    10.    at junit.framework.Assert.fail(Assert.java:47)    
    11.    at junit.framework.Assert.failNotEquals(Assert.java:282)    
    12.    at junit.framework.Assert.assertEquals(Assert.java:64)    
    13.    at junit.framework.Assert.assertEquals(Assert.java:149)    
    14.    at junit.framework.Assert.assertEquals(Assert.java:155)    
    15.    at SingletonTest$SingletonTestRunnable.run(Unknown Source)    
    16.    at java.lang.Thread.run(Thread.java:554)    
    17.      [java] .    
    18.      [java] Time: 0.577    
    19.      
    20.      [java] OK (1 test)   


    到現(xiàn)在為止我們已經(jīng)知道例4不是線程安全的,那就讓我們看看如何修正它。


    同步

    要使例4的單例類為線程安全的很容易----只要像下面一個同步化getInstance()方法:
    Java代碼
    1. public synchronized static Singleton getInstance() {    
    2.    if(singleton == null) {    
    3.       simulateRandomActivity();    
    4.       singleton = new Singleton();    
    5.    }    
    6.    logger.info("created singleton: " + singleton);    
    7.    return singleton;    
    8. }   

    在同步化getInstance()方法后,我們就可以得到例5的測試案例返回的下面的結(jié)果:
    Java代碼
    1. Buildfile: build.xml    
    2.      
    3. init:    
    4.      [echo] Build 20030414 (14-04-2003 03:15)    
    5.      
    6. compile:    
    7.     [javac] Compiling 2 source files    
    8.      
    9. run-test-text:    
    10. INFO Thread-1: sleeping...    
    11. INFO Thread-1: created singleton: Singleton@ef577d    
    12. INFO Thread-2: created singleton: Singleton@ef577d    
    13.      [java] .    
    14.      [java] Time: 0.513    
    15.      
    16.      [java] OK (1 test)   


    這此,這個測試案例工作正常,并且多線程的煩惱也被解決;然而,機敏的讀者可能會認識到getInstance()方法只需要在第一次被調(diào)用時同步。因為同步的性能開銷很昂貴(同步方法比非同步方法能降低到100次左右),或許我們可以引入一種性能改進方法,它只同步單例類的getInstance()方法中的賦值語句。

    一種性能改進的方法

    尋找一種性能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法:
    Java代碼
    1. public static Singleton getInstance() {    
    2.    if(singleton == null) {    
    3.       synchronized(Singleton.class) {     
    4.          singleton = new Singleton();    
    5.       }    
    6.    }    
    7.    return singleton;    
    8. }   


    這個代碼片段只同步了關(guān)鍵的代碼,而不是同步整個方法。然而這段代碼卻不是線程安全的。考慮一下下面的假定:線程1進入同步塊,并且在它給singleton成員變量賦值之前線程1被切換。接著另一個線程進入if塊。第二個線程將等待直到第一個線程完成,并且仍然會得到兩個不同的單例類實例。有修復(fù)這個問題的方法嗎?請讀下去。

    雙重加鎖檢查

    初看上去,雙重加鎖檢查似乎是一種使懶漢式實例化為線程安全的技術(shù)。下面的代碼片段展示了這種技術(shù):
    Java代碼
    1. public static Singleton getInstance() {    
    2.   if(singleton == null) {    
    3.      synchronized(Singleton.class) {    
    4.        if(singleton == null) {    
    5.          singleton = new Singleton();    
    6.        }    
    7.     }    
    8.   }    
    9.   return singleton;    
    10. }   


    如果兩個線程同時訪問getInstance()方法會發(fā)生什么?想像一下線程1進行同步塊馬上又被切換。接著,第二個線程進入if 塊。當(dāng)線程1退出同步塊時,線程2會重新檢查看是否singleton實例仍然為null。因為線程1設(shè)置了singleton成員變量,所以線程2的第二次檢查會失敗,第二個單例類實例也就不會被創(chuàng)建。似乎就是如此。
    不幸的是,雙重加鎖檢查不會保證正常工作,因為編譯器會在Singleton的構(gòu)造方法被調(diào)用之前隨意給singleton賦一個值。如果在singleton引用被賦值之后而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。

    一個改進的線程安全的單例模式實現(xiàn)

    例7列出了一個簡單、快速而又是線程安全的單例模式實現(xiàn):
    例7.一個簡單的單例類
    Java代碼
    1. public class Singleton {    
    2.    public final static Singleton INSTANCE = new Singleton();    
    3.    private Singleton() {    
    4.          // Exists only to defeat instantiation.    
    5.       }    
    6. }   


    這段代碼是線程安全的是因為靜態(tài)成員變量一定會在類被第一次訪問時被創(chuàng)建。你得到了一個自動使用了懶漢式實例化的線程安全的實現(xiàn);你應(yīng)該這樣使用它:
    Java代碼
    1. Singleton singleton = Singleton.INSTANCE;    
    2. singleton.dothis();    
    3. singleton.dothat();    
    4. ...   


    當(dāng)然萬事并不完美,前面的Singleton只是一個折衷的方案;如果你使用那個實現(xiàn),你就無法改變它以便后來你可能想要允許多個單例類的實例。用一種更折哀的單例模式實現(xiàn)(通過一個getInstance()方法獲得實例)你可以改變這個方法以便返回一個唯一的實例或者是數(shù)百個實例中的一個.你不能用一個公開且是靜態(tài)的(public static)成員變量這樣做.

    你可以安全的使用例7的單例模式實現(xiàn)或者是例1的帶一個同步的getInstance()方法的實現(xiàn).然而,我們必須要研究另一個問題:你必須在編譯期指定這個單例類,這樣就不是很靈活.一個單例類的注冊表會讓我們在運行期指定一個單例類.

    使用注冊表
    使用一個單例類注冊表可以:

    在運行期指定單例類

    防止產(chǎn)生多個單例類子類的實例
    在例8的單例類中,保持了一個通過類名進行注冊的單例類注冊表:
    例8 帶注冊表的單例類

    Java代碼
    1. import java.util.HashMap;    
    2. import org.apache.log4j.Logger;    
    3.      
    4. public class Singleton {    
    5.    private static HashMap map = new HashMap();    
    6.    private static Logger logger = Logger.getRootLogger();    
    7.      
    8.    protected Singleton() {    
    9.       // Exists only to thwart instantiation    
    10.    }    
    11.    public static synchronized Singleton getInstance(String classname) {    
    12.       if(classname == nullthrow new IllegalArgumentException("Illegal classname");    
    13.          Singleton singleton = (Singleton)map.get(classname);    
    14.      
    15.       if(singleton != null) {    
    16.          logger.info("got singleton from map: " + singleton);    
    17.          return singleton;    
    18.       }    
    19.       if(classname.equals("SingeltonSubclass_One"))    
    20.             singleton = new SingletonSubclass_One();             
    21.          else if(classname.equals("SingeltonSubclass_Two"))    
    22.             singleton = new SingletonSubclass_Two();    
    23.      
    24.       map.put(classname, singleton);    
    25.       logger.info("created singleton: " + singleton);    
    26.       return singleton;    
    27.    }    
    28.    // Assume functionality follows that's attractive to inherit    
    29. }   


    這段代碼的基類首先創(chuàng)建出子類的實例,然后把它們存儲在一個Map中。但是基類卻得付出很高的代價因為你必須為每一個子類替換它的getInstance()方法。幸運的是我們可以使用反射處理這個問題。

    使用反射

    在例9的帶注冊表的單例類中,使用反射來實例化一個特殊的類的對象。與例8相對的是通過這種實現(xiàn),Singleton.getInstance()方法不需要在每個被實現(xiàn)的子類中重寫了。
    例9 使用反射實例化單例類
    Java代碼
    1. import java.util.HashMap;    
    2. import org.apache.log4j.Logger;    
    3.      
    4. public class Singleton {    
    5.    private static HashMap map = new HashMap();    
    6.    private static Logger logger = Logger.getRootLogger();    
    7.      
    8.    protected Singleton() {    
    9.       // Exists only to thwart instantiation    
    10.    }    
    11.    public static synchronized Singleton getInstance(String classname) {    
    12.       Singleton singleton = (Singleton)map.get(classname);    
    13.      
    14.       if(singleton != null) {    
    15.          logger.info("got singleton from map: " + singleton);    
    16.          return singleton;    
    17.       }    
    18.       try {    
    19.          singleton = (Singleton)Class.forName(classname).newInstance();    
    20.       }    
    21.       catch(ClassNotFoundException cnf) {    
    22.          logger.fatal("Couldn't find class " + classname);        
    23.       }    
    24.       catch(InstantiationException ie) {    
    25.          logger.fatal("Couldn't instantiate an object of type " + classname);        
    26.       }    
    27.       catch(IllegalAccessException ia) {    
    28.          logger.fatal("Couldn't access class " + classname);        
    29.       }    
    30.       map.put(classname, singleton);    
    31.       logger.info("created singleton: " + singleton);    
    32.      
    33.       return singleton;    
    34.    }    
    35. }   


    關(guān)于單例類的注冊表應(yīng)該說明的是:它們應(yīng)該被封裝在它們自己的類中以便最大限度的進行復(fù)用。


    封裝注冊表

    例10列出了一個單例注冊表類。
    例10 一個SingletonRegistry類

    Java代碼
    1. import java.util.HashMap;    
    2. import org.apache.log4j.Logger;    
    3.      
    4. public class SingletonRegistry {    
    5.    public static SingletonRegistry REGISTRY = new SingletonRegistry();    
    6.      
    7.    private static HashMap map = new HashMap();    
    8.    private static Logger logger = Logger.getRootLogger();    
    9.      
    10.    protected SingletonRegistry() {    
    11.       // Exists to defeat instantiation    
    12.    }    
    13.    public static synchronized Object getInstance(String classname) {    
    14.       Object singleton = map.get(classname);    
    15.      
    16.       if(singleton != null) {    
    17.          return singleton;    
    18.       }    
    19.       try {    
    20.          singleton = Class.forName(classname).newInstance();    
    21.          logger.info("created singleton: " + singleton);    
    22.       }    
    23.       catch(ClassNotFoundException cnf) {    
    24.          logger.fatal("Couldn't find class " + classname);        
    25.       }    
    26.       catch(InstantiationException ie) {    
    27.          logger.fatal("Couldn't instantiate an object of type " +     
    28.                        classname);        
    29.       }    
    30.       catch(IllegalAccessException ia) {    
    31.          logger.fatal("Couldn't access class " + classname);        
    32.       }    
    33.       map.put(classname, singleton);    
    34.       return singleton;    
    35.    }    
    36. }   


    注意我是把SingletonRegistry類作為一個單例模式實現(xiàn)的。我也通用化了這個注冊表以便它能存儲和取回任何類型的對象。例11顯示了的Singleton類使用了這個注冊表。
    例11 使用了一個封裝的注冊表的Singleton類

    Java代碼
    1. import java.util.HashMap;    
    2. import org.apache.log4j.Logger;    
    3.      
    4. public class Singleton {    
    5.      
    6.    protected Singleton() {    
    7.       // Exists only to thwart instantiation.    
    8.    }    
    9.    public static Singleton getInstance() {    
    10.       return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);    
    11.    }    
    12. }   


    上面的Singleton類使用那個注冊表的唯一實例通過類名取得單例對象。
    現(xiàn)在我們已經(jīng)知道如何實現(xiàn)線程安全的單例類和如何使用一個注冊表去在運行期指定單例類名,接著讓我們考查一下如何安排類載入器和處理序列化。

    Classloaders

    在許多情況下,使用多個類載入器是很普通的--包括servlet容器--所以不管你在實現(xiàn)你的單例類時是多么小心你都最終可以得到多個單例類的實例。如果你想要確保你的單例類只被同一個的類載入器裝入,那你就必須自己指定這個類載入器;例如:

    Java代碼
    1. private static Class getClass(String classname)     
    2.                                          throws ClassNotFoundException {    
    3.       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();    
    4.      
    5.       if(classLoader == null)    
    6.          classLoader = Singleton.class.getClassLoader();    
    7.      
    8.       return (classLoader.loadClass(classname));    
    9.    }    
    10. }   


    這個方法會嘗試把當(dāng)前的線程與那個類載入器相關(guān)聯(lián);如果classloader為null,這個方法會使用與裝入單例類基類的那個類載入器。這個方法可以用Class.forName()代替。

    序列化

    如果你序列化一個單例類,然后兩次重構(gòu)它,那么你就會得到那個單例類的兩個實例,除非你實現(xiàn)readResolve()方法,像下面這樣:
    例12 一個可序列化的單例類

    Java代碼
    1. import org.apache.log4j.Logger;    
    2.      
    3. public class Singleton implements java.io.Serializable {    
    4.    public static Singleton INSTANCE = new Singleton();    
    5.      
    6.    protected Singleton() {    
    7.       // Exists only to thwart instantiation.    
    8.    }    
    9.    private Object readResolve() {    
    10.             return INSTANCE;    
    11.       }   
    12.    }   


    上面的單例類實現(xiàn)從readResolve()方法中返回一個唯一的實例;這樣無論Singleton類何時被重構(gòu),它都只會返回那個相同的單例類實例。
    例13測試了例12的單例類:
    例13 測試一個可序列化的單例類

    Java代碼
    1. import java.io.*;    
    2. import org.apache.log4j.Logger;    
    3. import junit.framework.Assert;    
    4. import junit.framework.TestCase;    
    5.      
    6. public class SingletonTest extends TestCase {    
    7.    private Singleton sone = null, stwo = null;    
    8.    private static Logger logger = Logger.getRootLogger();    
    9.      
    10.    public SingletonTest(String name) {    
    11.       super(name);    
    12.    }    
    13.    public void setUp() {    
    14.       sone = Singleton.INSTANCE;    
    15.       stwo = Singleton.INSTANCE;    
    16.    }    
    17.    public void testSerialize() {    
    18.       logger.info("testing singleton serialization...");    
    19. [b]      writeSingleton();    
    20.       Singleton s1 = readSingleton();    
    21.       Singleton s2 = readSingleton();    
    22.       Assert.assertEquals(true, s1 == s2);[/b]   }    
    23.    private void writeSingleton() {    
    24.       try {    
    25.          FileOutputStream fos = new FileOutputStream("serializedSingleton");    
    26.          ObjectOutputStream oos = new ObjectOutputStream(fos);    
    27.          Singleton s = Singleton.INSTANCE;    
    28.      
    29.          oos.writeObject(Singleton.INSTANCE);    
    30.          oos.flush();    
    31.       }    
    32.       catch(NotSerializableException se) {    
    33.          logger.fatal("Not Serializable Exception: " + se.getMessage());    
    34.       }    
    35.       catch(IOException iox) {    
    36.          logger.fatal("IO Exception: " + iox.getMessage());    
    37.       }    
    38.    }    
    39.    private Singleton readSingleton() {    
    40.       Singleton s = null;    
    41.      
    42.       try {    
    43.          FileInputStream fis = new FileInputStream("serializedSingleton");    
    44.          ObjectInputStream ois = new ObjectInputStream(fis);    
    45.          s = (Singleton)ois.readObject();    
    46.       }    
    47.       catch(ClassNotFoundException cnf) {    
    48.          logger.fatal("Class Not Found Exception: " + cnf.getMessage());    
    49.       }    
    50.       catch(NotSerializableException se) {    
    51.          logger.fatal("Not Serializable Exception: " + se.getMessage());    
    52.       }    
    53.       catch(IOException iox) {    
    54.          logger.fatal("IO Exception: " + iox.getMessage());    
    55.       }    
    56.       return s;    
    57.    }    
    58.    public void testUnique() {    
    59.       logger.info("testing singleton uniqueness...");    
    60.       Singleton another = new Singleton();    
    61.      
    62.       logger.info("checking singletons for equality");    
    63.       Assert.assertEquals(true, sone == stwo);    
    64.    }    
    65. }   


    前面這個測試案例序列化例12中的單例類,并且兩次重構(gòu)它。然后這個測試案例檢查看是否被重構(gòu)的單例類實例是同一個對象。下面是測試案例的輸出:

    Java代碼
    1. Buildfile: build.xml    
    2.      
    3. init:    
    4.      [echo] Build 20030422 (22-04-2003 11:32)    
    5.      
    6. compile:    
    7.      
    8. run-test-text:    
    9.      [java] .INFO main: testing singleton serialization...    
    10.      [java] .INFO main: testing singleton uniqueness...    
    11.      [java] INFO main: checking singletons for equality    
    12.      
    13.      [java] Time: 0.1    
    14.      
    15.      [java] OK (2 tests)   


    單例模式結(jié)束語

    單例模式簡單卻容易讓人迷惑,特別是對于Java的開發(fā)者來說。在這篇文章中,作者演示了Java開發(fā)者在顧及多線程、類載入器和序列化情況如何實現(xiàn)單例模式。作者也展示了你怎樣才能實現(xiàn)一個單例類的注冊表,以便能夠在運行期指定單例類。
    posted on 2009-04-06 18:35 lanxin1020 閱讀(155) 評論(0)  編輯  收藏 所屬分類: j2se
    主站蜘蛛池模板: 日本中文字幕免费高清视频| 国产AV无码专区亚洲精品| 午夜免费福利小电影| 免费视频成人国产精品网站| 亚洲一区中文字幕在线观看| 亚洲av无码乱码国产精品| 免费成人午夜视频| 岛国av无码免费无禁网站| 日韩免费在线观看视频| 久久av免费天堂小草播放| 无码色偷偷亚洲国内自拍| 一本色道久久88亚洲精品综合 | 亚洲AV日韩AV永久无码免下载| 国产精品久免费的黄网站| 在线观看AV片永久免费| 7m凹凸精品分类大全免费| 美女在线视频观看影院免费天天看 | 国产精品手机在线亚洲| 亚洲乱码在线观看| 亚洲精品免费在线| 激情内射亚洲一区二区三区| 久久亚洲精品成人777大小说| 亚洲精品无码不卡在线播HE| 国产亚洲精品a在线观看| 亚洲午夜福利精品无码| 亚洲成AV人网址| 成人亚洲网站www在线观看| 国产国产人免费视频成69大陆 | 亚洲精品美女久久久久久久| 亚洲avav天堂av在线网爱情| 亚洲手机中文字幕| 亚洲AV无码成人专区| 在线亚洲高清揄拍自拍一品区| 亚洲av产在线精品亚洲第一站| 久久精品国产亚洲AV蜜臀色欲 | 久久久高清免费视频| 18禁超污无遮挡无码免费网站国产| 成人免费视频一区二区三区| 日韩在线视频免费看| 免费中文字幕在线| 亚洲第一页综合图片自拍|