java 單例模式 (Singleton)

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

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


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

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

單例模式 

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

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

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


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

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

測試單例模式 

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

例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兩次調用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: }   


如果一個線程在第二行的賦值語句發生之前切換,那麼成員變量instance仍然是null,然後另一個線程可能接下來進入到if塊中。在這種情況下,兩個不同的單例類實例就被創建。不幸的是這種假定很少發生,這樣這種假定也很難在測試期間出現(譯註:在這可能是作者對很少出現這種情況而導致無法測試從而使人們放鬆警惕而感到嘆惜)。爲了演示這個線程輪換,我得重新實現例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()方法第一次被調用時,調用這個方法的線程會休眠50毫秒以便另外的線程也有時間調用getInstance()並創建一個新的單例類實例。當休眠的線程覺醒時,它也會創建一個新的單例類實例,這樣我們就有兩個單例類實例。儘管例4是人爲如此的,但它卻模擬了第一個線程調用了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的測試案例創建兩個線程,然後各自啓動,等待完成。這個案例保持了一個對單例類的靜態引用,每個線程都會調用Singleton.getInstance()。如果這個靜態成員變量沒有被設置,那麼第一個線程就會將它設爲通過調用getInstance()而得到的引用,然後這個靜態變量會與一個局部變量比較是否相等。 
在這個測試案例運行時會發生一系列的事情:第一個線程調用getInstance(),進入if塊,然後休眠;接着,第二個線程也調用getInstance()並且創建了一個單例類的實例。第二個線程會設置這個靜態成員變量爲它所創建的引用。第二個線程檢查這個靜態成員變量與一個局部備份的相等性。然後測試通過。當第一個線程覺醒時,它也會創建一個單例類的實例,並且它不會設置那個靜態成員變量(因爲第二個線程已經設置過了),所以那個靜態變量與那個局部變量脫離同步,相等性測試即告失敗。例6列出了例5的輸出: 
例6.例5的輸出 

Java代碼  收藏代碼
  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)   


到現在爲止我們已經知道例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的測試案例返回的下面的結果: 
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()方法只需要在第一次被調用時同步。因爲同步的性能開銷很昂貴(同步方法比非同步方法能降低到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. }   


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

雙重加鎖檢查 

初看上去,雙重加鎖檢查似乎是一種使懶漢式實例化爲線程安全的技術。下面的代碼片段展示了這種技術: 
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()方法會發生什麼?想像一下線程1進行同步塊馬上又被切換。接着,第二個線程進入if 塊。當線程1退出同步塊時,線程2會重新檢查看是否singleton實例仍然爲null。因爲線程1設置了singleton成員變量,所以線程2的第二次檢查會失敗,第二個單例類實例也就不會被創建。似乎就是如此。 
不幸的是,雙重加鎖檢查不會保證正常工作,因爲編譯器會在Singleton的構造方法被調用之前隨意給singleton賦一個值。如果在singleton引用被賦值之後而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。 

一個改進的線程安全的單例模式實現 

例7列出了一個簡單、快速而又是線程安全的單例模式實現: 
例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. }   


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


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

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

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

在運行期指定單例類 

防止產生多個單例類子類的實例 
在例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. }   


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

使用反射 

在例9的帶註冊表的單例類中,使用反射來實例化一個特殊的類的對象。與例8相對的是通過這種實現,Singleton.getInstance()方法不需要在每個被實現的子類中重寫了。 
例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. }   


關於單例類的註冊表應該說明的是:它們應該被封裝在它們自己的類中以便最大限度的進行復用。 


封裝註冊表 

例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類作爲一個單例模式實現的。我也通用化了這個註冊表以便它能存儲和取回任何類型的對象。例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類使用那個註冊表的唯一實例通過類名取得單例對象。 
現在我們已經知道如何實現線程安全的單例類和如何使用一個註冊表去在運行期指定單例類名,接着讓我們考查一下如何安排類載入器和處理序列化。 

Classloaders 

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

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. }   


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

序列化 

如果你序列化一個單例類,然後兩次重構它,那麼你就會得到那個單例類的兩個實例,除非你實現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.    }   


上面的單例類實現從readResolve()方法中返回一個唯一的實例;這樣無論Singleton類何時被重構,它都只會返回那個相同的單例類實例。 
例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中的單例類,並且兩次重構它。然後這個測試案例檢查看是否被重構的單例類實例是同一個對象。下面是測試案例的輸出: 

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)   


單例模式結束語 

單例模式簡單卻容易讓人迷惑,特別是對於Java的開發者來說。在這篇文章中,作者演示了Java開發者在顧及多線程、類載入器和序列化情況如何實現單例模式。作者也展示了你怎樣才能實現一個單例類的註冊表,以便能夠在運行期指定單例類。 
發佈了52 篇原創文章 · 獲贊 178 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章