概要
單例模式是最簡單的設計模式之一,但是對于Java的開發者來說,它卻有很多缺陷。在本月的專欄中,David Geary探討了單例模式以及在面對多線程(multithreading)、類裝載器(classloaders)和序列化(serialization)時如何處理這些缺陷。
單例模式適合于一個類只有一個實例的情況,比如窗口管理器,打印緩沖池和文件系統,它們都是原型的例子。典型的情況是,那些對象的類型被遍及一個軟件系統的不同對象訪問,因此需要一個全局的訪問指針,這便是眾所周知的單例模式的應用。當然這只有在你確信你不再需要任何多于一個的實例的情況下。
單例模式的用意在于前一段中所關心的。通過單例模式你可以:
確保一個類只有一個實例被建立
提供了一個對對象的全局訪問指針
在不影響單例類的客戶端的情況下允許將來有多個實例
盡管單例設計模式如在下面的圖中的所顯示的一樣是最簡單的設計模式,但對于粗心的Java開發者來說卻呈現出許多缺陷。這篇文章討論了單例模式并揭示了那些缺陷。
注意:你可以從
Resources
下載這篇文章的源代碼。
單例模式
在《設計模式》一書中,作者這樣來敘述單例模式的:確保一個類只有一個實例并提供一個對它的全局訪問指針。
下圖說明了單例模式的類圖。
(圖1)
單例模式的類圖
正如你在上圖中所看到的,這不是單例模式的完整部分。此圖中單例類保持了一個對唯一的單例實例的靜態引用,并且會從靜態getInstance()方法中返回對那個實例的引用。
例1顯示了一個經典的單例模式的實現。
例1.經典的單例模式
- public class ClassicSingleton{
- private static ClassicSingletoninstance= null ;
- protected ClassicSingleton(){
- //Existsonlytodefeatinstantiation.
- }
- public static ClassicSingletongetInstance(){
- if (instance== null ){
- instance= new ClassicSingleton();
- }
- return instance;
- }
- }
public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }
在例1中的單例模式的實現很容易理解。ClassicSingleton類保持了一個對單獨的單例實例的靜態引用,并且從靜態方法getInstance()中返回那個引用。
關于ClassicSingleton類,有幾個讓我們感興趣的地方。首先,ClassicSingleton使用了一個眾所周知的懶漢式實例化去創建那個單例類的引用;結果,這個單例類的實例直到getInstance()方法被第一次調用時才被創建。這種技巧可以確保單例類的實例只有在需要時才被建立出來。其次,注意ClassicSingleton實現了一個protected的構造方法,這樣客戶端不能直接實例化一個ClassicSingleton類的實例。然而,你會驚奇的發現下面的代碼完全合法:
- public class SingletonInstantiator{
- public SingletonInstantiator(){
- ClassicSingletoninstance=ClassicSingleton.getInstance();
- ClassicSingletonanotherInstance=
- new ClassicSingleton();
- ...
- }
- }
public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance = new ClassicSingleton(); ... } }
前面這個代碼片段為何能在沒有繼承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.一個單例模式的案例
- import org.apache.log4j.Logger;
- import junit.framework.Assert;
- import junit.framework.TestCase;
- public class SingletonTest extends TestCase{
- private ClassicSingletonsone= null ,stwo= null ;
- private static Loggerlogger=Logger.getRootLogger();
- public SingletonTest(Stringname){
- super (name);
- }
- public void setUp(){
- logger.info( "gettingsingleton..." );
- sone=ClassicSingleton.getInstance();
- logger.info( "...gotsingleton:" +sone);
- logger.info( "gettingsingleton..." );
- stwo=ClassicSingleton.getInstance();
- logger.info( "...gotsingleton:" +stwo);
- }
- public void testUnique(){
- logger.info( "checkingsingletonsforequality" );
- Assert.assertEquals( true ,sone==stwo);
- }
- }
import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }
例2兩次調用ClassicSingleton.getInstance(),并且把返回的引用存儲在成員變量中。方法testUnique()會檢查這些引用看它們是否相同。例3是這個測試案例的輸出:
例3.是這個測試案例的輸出
- Buildfile:build.xml
- init:
- [echo]Build 20030414 ( 14 - 04 - 2003 03 : 08 )
- compile:
- run-test-text:
- [java].INFOmain:[b]gettingsingleton...[/b]
- [java]INFOmain:[b]createdsingleton:[/b]Singleton @e86f41
- [java]INFOmain:...gotsingleton:Singleton @e86f41
- [java]INFOmain:[b]gettingsingleton...[/b]
- [java]INFOmain:...gotsingleton:Singleton @e86f41
- [java]INFOmain:checkingsingletons for equality
- [java]Time: 0.032
- [java]OK( 1 test)
Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: [b]getting singleton...[/b] [java] INFO main: [b]created singleton:[/b] Singleton@e86f41 [java] INFO main: ...got singleton: Singleton@e86f41 [java] INFO main: [b]getting singleton...[/b] [java] INFO main: ...got singleton: Singleton@e86f41 [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)
正如前面的清單所示,例2的簡單測試順利通過----通過ClassicSingleton.getInstance()獲得的兩個單例類的引用確實相同;然而,你要知道這些引用是在單線程中得到的。下面的部分著重于用多線程測試單例類。
多線程因素的考慮
在例1中的ClassicSingleton.getInstance()方法由于下面的代碼而不是線程安全的:
- 1 : if (instance== null ){
- 2 :instance= new Singleton();
- 3 :}
1: if(instance == null) { 2: instance = new Singleton(); 3: }
如果一個線程在第二行的賦值語句發生之前切換,那么成員變量instance仍然是null,然后另一個線程可能接下來進入到if塊中。在這種情況下,兩個不同的單例類實例就被創建。不幸的是這種假定很少發生,這樣這種假定也很難在測試期間出現(譯注:在這可能是作者對很少出現這種情況而導致無法測試從而使人們放松警惕而感到嘆惜)。為了演示這個線程輪換,我得重新實現例1中的那個類。例4就是修訂后的單例類:
例4.人為安排的方式
- import org.apache.log4j.Logger;
- public class Singleton{
- private static Singletonsingleton= null ;
- private static Loggerlogger=Logger.getRootLogger();
- private static boolean firstThread= true ;
- protected Singleton(){
- //Existsonlytodefeatinstantiation.
- }
- public static SingletongetInstance(){
- if (singleton== null ){
- simulateRandomActivity();
- singleton= new Singleton();
- }
- logger.info( "createdsingleton:" +singleton);
- return singleton;
- }
- private static void simulateRandomActivity(){
- try {
- if (firstThread){
- firstThread= false ;
- logger.info( "sleeping..." );
- //Thisnapshouldgivethesecondthreadenoughtime
- //togetbythefirstthread.
- Thread.currentThread().sleep( 50 );
- }
- }
- catch (InterruptedExceptionex){
- logger.warn( "Sleepinterrupted" );
- }
- }
- }
import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread. Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }
除了在這個清單中的單例類強制使用了一個多線程錯誤處理,例4類似于例1中的單例類。在getInstance()方法第一次被調用時,調用這個方法的線程會休眠50毫秒以便另外的線程也有時間調用getInstance()并創建一個新的單例類實例。當休眠的線程覺醒時,它也會創建一個新的單例類實例,這樣我們就有兩個單例類實例。盡管例4是人為如此的,但它卻模擬了第一個線程調用了getInstance()并在沒有完成時被切換的真實情形。
例5測試了例4的單例類:
例5.失敗的測試
- import org.apache.log4j.Logger;
- import junit.framework.Assert;
- import junit.framework.TestCase;
- public class SingletonTest extends TestCase{
- private static Loggerlogger=Logger.getRootLogger();
- private static Singletonsingleton= null ;
- public SingletonTest(Stringname){
- super (name);
- }
- public void setUp(){
- singleton= null ;
- }
- public void testUnique() throws InterruptedException{
- //BoththreadscallSingleton.getInstance().
- ThreadthreadOne= new Thread( new SingletonTestRunnable()),
- threadTwo= new Thread( new SingletonTestRunnable());
- threadOne.start();
- threadTwo.start();
- threadOne.join();
- threadTwo.join();
- }
- private static class SingletonTestRunnable implements Runnable{
- public void run(){
- //Getareferencetothesingleton.
- Singletons=Singleton.getInstance();
- //Protectsingletonmembervariablefrom
- //multithreadedaccess.
- synchronized (SingletonTest. class ){
- if (singleton== null ) //Iflocalreferenceisnull...
- singleton=s; //...setittothesingleton
- }
- //Localreferencemustbeequaltotheoneand
- //onlyinstanceofSingleton;otherwise,wehavetwo
- //Singletoninstances.
- Assert.assertEquals( true ,s==singleton);
- }
- }
- }
import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }
例5的測試案例創建兩個線程,然后各自啟動,等待完成。這個案例保持了一個對單例類的靜態引用,每個線程都會調用Singleton.getInstance()。如果這個靜態成員變量沒有被設置,那么第一個線程就會將它設為通過調用getInstance()而得到的引用,然后這個靜態變量會與一個局部變量比較是否相等。
在這個測試案例運行時會發生一系列的事情:第一個線程調用getInstance(),進入if塊,然后休眠;接著,第二個線程也調用getInstance()并且創建了一個單例類的實例。第二個線程會設置這個靜態成員變量為它所創建的引用。第二個線程檢查這個靜態成員變量與一個局部備份的相等性。然后測試通過。當第一個線程覺醒時,它也會創建一個單例類的實例,并且它不會設置那個靜態成員變量(因為第二個線程已經設置過了),所以那個靜態變量與那個局部變量脫離同步,相等性測試即告失敗。例6列出了例5的輸出:
例6.例5的輸出
- Buildfile:build.xml
- init:
- [echo]Build 20030414 ( 14 - 04 - 2003 03 : 06 )
- compile:
- run-test-text:
- INFOThread- 1 :sleeping...
- INFOThread- 2 :createdsingleton:Singleton @7e5cbd
- INFOThread- 1 :createdsingleton:Singleton @704ebb
- junit.framework.AssertionFailedError:expected:butwas:
- atjunit.framework.Assert.fail(Assert.java: 47 )
- atjunit.framework.Assert.failNotEquals(Assert.java: 282 )
- atjunit.framework.Assert.assertEquals(Assert.java: 64 )
- atjunit.framework.Assert.assertEquals(Assert.java: 149 )
- atjunit.framework.Assert.assertEquals(Assert.java: 155 )
- atSingletonTest$SingletonTestRunnable.run(UnknownSource)
- atjava.lang.Thread.run(Thread.java: 554 )
- [java].
- [java]Time: 0.577
- [java]OK( 1 test)
Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:06) compile: run-test-text: INFO Thread-1: sleeping... INFO Thread-2: created singleton: Singleton@7e5cbd INFO Thread-1: created singleton: Singleton@704ebb junit.framework.AssertionFailedError: expected: but was: at junit.framework.Assert.fail(Assert.java:47) at junit.framework.Assert.failNotEquals(Assert.java:282) at junit.framework.Assert.assertEquals(Assert.java:64) at junit.framework.Assert.assertEquals(Assert.java:149) at junit.framework.Assert.assertEquals(Assert.java:155) at SingletonTest$SingletonTestRunnable.run(Unknown Source) at java.lang.Thread.run(Thread.java:554) [java] . [java] Time: 0.577 [java] OK (1 test)
到現在為止我們已經知道例4不是線程安全的,那就讓我們看看如何修正它。
同步
要使例4的單例類為線程安全的很容易----只要像下面一個同步化getInstance()方法:
- public synchronized static SingletongetInstance(){
- if (singleton== null ){
- simulateRandomActivity();
- singleton= new Singleton();
- }
- logger.info( "createdsingleton:" +singleton);
- return singleton;
- }
public synchronized static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; }
在同步化getInstance()方法后,我們就可以得到例5的測試案例返回的下面的結果:
- Buildfile:build.xml
- init:
- [echo]Build 20030414 ( 14 - 04 - 2003 03 : 15 )
- compile:
- [javac]Compiling 2 sourcefiles
- run-test-text:
- INFOThread- 1 :sleeping...
- INFOThread- 1 :createdsingleton:Singleton @ef577d
- INFOThread- 2 :createdsingleton:Singleton @ef577d
- [java].
- [java]Time: 0.513
- [java]OK( 1 test)
Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:15) compile: [javac] Compiling 2 source files run-test-text: INFO Thread-1: sleeping... INFO Thread-1: created singleton: Singleton@ef577d INFO Thread-2: created singleton: Singleton@ef577d [java] . [java] Time: 0.513 [java] OK (1 test)
這此,這個測試案例工作正常,并且多線程的煩惱也被解決;然而,機敏的讀者可能會認識到getInstance()方法只需要在第一次被調用時同步。因為同步的性能開銷很昂貴(同步方法比非同步方法能降低到100次左右),或許我們可以引入一種性能改進方法,它只同步單例類的getInstance()方法中的賦值語句。
一種性能改進的方法
尋找一種性能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法:
- public static SingletongetInstance(){
- if (singleton== null ){
- synchronized (Singleton. class ){
- singleton= new Singleton();
- }
- }
- return singleton;
- }
public static Singleton getInstance() { if(singleton == null) { synchronized(Singleton.class) { singleton = new Singleton(); } } return singleton; }
這個代碼片段只同步了關鍵的代碼,而不是同步整個方法。然而這段代碼卻不是線程安全的。考慮一下下面的假定:線程1進入同步塊,并且在它給singleton成員變量賦值之前線程1被切換。接著另一個線程進入if塊。第二個線程將等待直到第一個線程完成,并且仍然會得到兩個不同的單例類實例。有修復這個問題的方法嗎?請讀下去。
雙重加鎖檢查
初看上去,雙重加鎖檢查似乎是一種使懶漢式實例化為線程安全的技術。下面的代碼片段展示了這種技術:
- public static SingletongetInstance(){
- if (singleton== null ){
- synchronized (Singleton. class ){
- if (singleton== null ){
- singleton= new Singleton();
- }
- }
- }
- return singleton;
- }
public static Singleton getInstance() { if(singleton == null) { synchronized(Singleton.class) { if(singleton == null) { singleton = new Singleton(); } } } return singleton; }
如果兩個線程同時訪問getInstance()方法會發生什么?想像一下線程1進行同步塊馬上又被切換。接著,第二個線程進入if 塊。當線程1退出同步塊時,線程2會重新檢查看是否singleton實例仍然為null。因為線程1設置了singleton成員變量,所以線程2的第二次檢查會失敗,第二個單例類實例也就不會被創建。似乎就是如此。
不幸的是,雙重加鎖檢查不會保證正常工作,因為編譯器會在Singleton的構造方法被調用之前隨意給singleton賦一個值。如果在singleton引用被賦值之后而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。
一個改進的線程安全的單例模式實現
例7列出了一個簡單、快速而又是線程安全的單例模式實現:
例7.一個簡單的單例類
- public class Singleton{
- public final static SingletonINSTANCE= new Singleton();
- private Singleton(){
- //Existsonlytodefeatinstantiation.
- }
- }
public class Singleton { public final static Singleton INSTANCE = new Singleton(); private Singleton() { // Exists only to defeat instantiation. } }
這段代碼是線程安全的是因為靜態成員變量一定會在類被第一次訪問時被創建。你得到了一個自動使用了懶漢式實例化的線程安全的實現;你應該這樣使用它:
- Singletonsingleton=Singleton.INSTANCE;
- singleton.dothis();
- singleton.dothat();
- ...
Singleton singleton = Singleton.INSTANCE; singleton.dothis(); singleton.dothat(); ...
當然萬事并不完美,前面的Singleton只是一個折衷的方案;如果你使用那個實現,你就無法改變它以便后來你可能想要允許多個單例類的實例。用一種更折哀的單例模式實現(通過一個getInstance()方法獲得實例)你可以改變這個方法以便返回一個唯一的實例或者是數百個實例中的一個.你不能用一個公開且是靜態的(public static)成員變量這樣做.
你可以安全的使用例7的單例模式實現或者是例1的帶一個同步的getInstance()方法的實現.然而,我們必須要研究另一個問題:你必須在編譯期指定這個單例類,這樣就不是很靈活.一個單例類的注冊表會讓我們在運行期指定一個單例類.
使用注冊表
使用一個單例類注冊表可以:
在運行期指定單例類
防止產生多個單例類子類的實例
在例8的單例類中,保持了一個通過類名進行注冊的單例類注冊表:
例8 帶注冊表的單例類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton{
- private static HashMapmap= new HashMap();
- private static Loggerlogger=Logger.getRootLogger();
- protected Singleton(){
- //Existsonlytothwartinstantiation
- }
- public static synchronized SingletongetInstance(Stringclassname){
- if (classname== null ) throw new IllegalArgumentException( "Illegalclassname" );
- Singletonsingleton=(Singleton)map.get(classname);
- if (singleton!= null ){
- logger.info( "gotsingletonfrommap:" +singleton);
- return singleton;
- }
- if (classname.equals( "SingeltonSubclass_One" ))
- singleton= new SingletonSubclass_One();
- else if (classname.equals( "SingeltonSubclass_Two" ))
- singleton= new SingletonSubclass_Two();
- map.put(classname,singleton);
- logger.info( "createdsingleton:" +singleton);
- return singleton;
- }
- //Assumefunctionalityfollowsthat'sattractivetoinherit
- }
import java.util.HashMap; import org.apache.log4j.Logger; public class Singleton { private static HashMap map = new HashMap(); private static Logger logger = Logger.getRootLogger(); protected Singleton() { // Exists only to thwart instantiation } public static synchronized Singleton getInstance(String classname) { if(classname == null) throw new IllegalArgumentException("Illegal classname"); Singleton singleton = (Singleton)map.get(classname); if(singleton != null) { logger.info("got singleton from map: " + singleton); return singleton; } if(classname.equals("SingeltonSubclass_One")) singleton = new SingletonSubclass_One(); else if(classname.equals("SingeltonSubclass_Two")) singleton = new SingletonSubclass_Two(); map.put(classname, singleton); logger.info("created singleton: " + singleton); return singleton; } // Assume functionality follows that's attractive to inherit }
這段代碼的基類首先創建出子類的實例,然后把它們存儲在一個Map中。但是基類卻得付出很高的代價因為你必須為每一個子類替換它的getInstance()方法。幸運的是我們可以使用反射處理這個問題。
使用反射
在例9的帶注冊表的單例類中,使用反射來實例化一個特殊的類的對象。與例8相對的是通過這種實現,Singleton.getInstance()方法不需要在每個被實現的子類中重寫了。
例9 使用反射實例化單例類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton{
- private static HashMapmap= new HashMap();
- private static Loggerlogger=Logger.getRootLogger();
- protected Singleton(){
- //Existsonlytothwartinstantiation
- }
- public static synchronized SingletongetInstance(Stringclassname){
- Singletonsingleton=(Singleton)map.get(classname);
- if (singleton!= null ){
- logger.info( "gotsingletonfrommap:" +singleton);
- return singleton;
- }
- try {
- singleton=(Singleton)Class.forName(classname).newInstance();
- }
- catch (ClassNotFoundExceptioncnf){
- logger.fatal( "Couldn'tfindclass" +classname);
- }
- catch (InstantiationExceptionie){
- logger.fatal( "Couldn'tinstantiateanobjectoftype" +classname);
- }
- catch (IllegalAccessExceptionia){
- logger.fatal( "Couldn'taccessclass" +classname);
- }
- map.put(classname,singleton);
- logger.info( "createdsingleton:" +singleton);
- return singleton;
- }
- }
import java.util.HashMap; import org.apache.log4j.Logger; public class Singleton { private static HashMap map = new HashMap(); private static Logger logger = Logger.getRootLogger(); protected Singleton() { // Exists only to thwart instantiation } public static synchronized Singleton getInstance(String classname) { Singleton singleton = (Singleton)map.get(classname); if(singleton != null) { logger.info("got singleton from map: " + singleton); return singleton; } try { singleton = (Singleton)Class.forName(classname).newInstance(); } catch(ClassNotFoundException cnf) { logger.fatal("Couldn't find class " + classname); } catch(InstantiationException ie) { logger.fatal("Couldn't instantiate an object of type " + classname); } catch(IllegalAccessException ia) { logger.fatal("Couldn't access class " + classname); } map.put(classname, singleton); logger.info("created singleton: " + singleton); return singleton; } }
關于單例類的注冊表應該說明的是:它們應該被封裝在它們自己的類中以便最大限度的進行復用。
封裝注冊表
例10列出了一個單例注冊表類。
例10 一個SingletonRegistry類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class SingletonRegistry{
- public static SingletonRegistryREGISTRY= new SingletonRegistry();
- private static HashMapmap= new HashMap();
- private static Loggerlogger=Logger.getRootLogger();
- protected SingletonRegistry(){
- //Existstodefeatinstantiation
- }
- public static synchronized ObjectgetInstance(Stringclassname){
- Objectsingleton=map.get(classname);
- if (singleton!= null ){
- return singleton;
- }
- try {
- singleton=Class.forName(classname).newInstance();
- logger.info( "createdsingleton:" +singleton);
- }
- catch (ClassNotFoundExceptioncnf){
- logger.fatal( "Couldn'tfindclass" +classname);
- }
- catch (InstantiationExceptionie){
- logger.fatal( "Couldn'tinstantiateanobjectoftype" +
- classname);
- }
- catch (IllegalAccessExceptionia){
- logger.fatal( "Couldn'taccessclass" +classname);
- }
- map.put(classname,singleton);
- return singleton;
- }
- }
import java.util.HashMap; import org.apache.log4j.Logger; public class SingletonRegistry { public static SingletonRegistry REGISTRY = new SingletonRegistry(); private static HashMap map = new HashMap(); private static Logger logger = Logger.getRootLogger(); protected SingletonRegistry() { // Exists to defeat instantiation } public static synchronized Object getInstance(String classname) { Object singleton = map.get(classname); if(singleton != null) { return singleton; } try { singleton = Class.forName(classname).newInstance(); logger.info("created singleton: " + singleton); } catch(ClassNotFoundException cnf) { logger.fatal("Couldn't find class " + classname); } catch(InstantiationException ie) { logger.fatal("Couldn't instantiate an object of type " + classname); } catch(IllegalAccessException ia) { logger.fatal("Couldn't access class " + classname); } map.put(classname, singleton); return singleton; } }
注意我是把SingletonRegistry類作為一個單例模式實現的。我也通用化了這個注冊表以便它能存儲和取回任何類型的對象。例11顯示了的Singleton類使用了這個注冊表。
例11 使用了一個封裝的注冊表的Singleton類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton{
- protected Singleton(){
- //Existsonlytothwartinstantiation.
- }
- public static SingletongetInstance(){
- return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
- }
- }
import java.util.HashMap; import org.apache.log4j.Logger; public class Singleton { protected Singleton() { // Exists only to thwart instantiation. } public static Singleton getInstance() { return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname); } }
上面的Singleton類使用那個注冊表的唯一實例通過類名取得單例對象。
現在我們已經知道如何實現線程安全的單例類和如何使用一個注冊表去在運行期指定單例類名,接著讓我們考查一下如何安排類載入器和處理序列化。
Classloaders
在許多情況下,使用多個類載入器是很普通的--包括servlet容器--所以不管你在實現你的單例類時是多么小心你都最終可以得到多個單例類的實例。如果你想要確保你的單例類只被同一個的類載入器裝入,那你就必須自己指定這個類載入器;例如:
- private static ClassgetClass(Stringclassname)
- throws ClassNotFoundException{
- ClassLoaderclassLoader=Thread.currentThread().getContextClassLoader();
- if (classLoader== null )
- classLoader=Singleton. class .getClassLoader();
- return (classLoader.loadClass(classname));
- }
- }
private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if(classLoader == null) classLoader = Singleton.class.getClassLoader(); return (classLoader.loadClass(classname)); } }
這個方法會嘗試把當前的線程與那個類載入器相關聯;如果classloader為null,這個方法會使用與裝入單例類基類的那個類載入器。這個方法可以用Class.forName()代替。
序列化
如果你序列化一個單例類,然后兩次重構它,那么你就會得到那個單例類的兩個實例,除非你實現readResolve()方法,像下面這樣:
例12 一個可序列化的單例類
- import org.apache.log4j.Logger;
- public class Singleton implements java.io.Serializable{
- public static SingletonINSTANCE= new Singleton();
- protected Singleton(){
- //Existsonlytothwartinstantiation.
- }
- private ObjectreadResolve(){
- return INSTANCE;
- }
- }
import org.apache.log4j.Logger; public class Singleton implements java.io.Serializable { public static Singleton INSTANCE = new Singleton(); protected Singleton() { // Exists only to thwart instantiation. } private Object readResolve() { return INSTANCE; } }
上面的單例類實現從readResolve()方法中返回一個唯一的實例;這樣無論Singleton類何時被重構,它都只會返回那個相同的單例類實例。
例13測試了例12的單例類:
例13 測試一個可序列化的單例類
- import java.io.*;
- import org.apache.log4j.Logger;
- import junit.framework.Assert;
- import junit.framework.TestCase;
- public class SingletonTest extends TestCase{
- private Singletonsone= null ,stwo= null ;
- private static Loggerlogger=Logger.getRootLogger();
- public SingletonTest(Stringname){
- super (name);
- }
- public void setUp(){
- sone=Singleton.INSTANCE;
- stwo=Singleton.INSTANCE;
- }
- public void testSerialize(){
- logger.info( "testingsingletonserialization..." );
- [b]writeSingleton();
- Singletons1=readSingleton();
- Singletons2=readSingleton();
- Assert.assertEquals( true ,s1==s2);[/b]}
- private void writeSingleton(){
- try {
- FileOutputStreamfos= new FileOutputStream( "serializedSingleton" );
- ObjectOutputStreamoos= new ObjectOutputStream(fos);
- Singletons=Singleton.INSTANCE;
- oos.writeObject(Singleton.INSTANCE);
- oos.flush();
- }
- catch (NotSerializableExceptionse){
- logger.fatal( "NotSerializableException:" +se.getMessage());
- }
- catch (IOExceptioniox){
- logger.fatal( "IOException:" +iox.getMessage());
- }
- }
- private SingletonreadSingleton(){
- Singletons= null ;
- try {
- FileInputStreamfis= new FileInputStream( "serializedSingleton" );
- ObjectInputStreamois= new ObjectInputStream(fis);
- s=(Singleton)ois.readObject();
- }
- catch (ClassNotFoundExceptioncnf){
- logger.fatal( "ClassNotFoundException:" +cnf.getMessage());
- }
- catch (NotSerializableExceptionse){
- logger.fatal( "NotSerializableException:" +se.getMessage());
- }
- catch (IOExceptioniox){
- logger.fatal( "IOException:" +iox.getMessage());
- }
- return s;
- }
- public void testUnique(){
- logger.info( "testingsingletonuniqueness..." );
- Singletonanother= new Singleton();
- logger.info( "checkingsingletonsforequality" );
- Assert.assertEquals( true ,sone==stwo);
- }
- }
import java.io.*; import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private Singleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { sone = Singleton.INSTANCE; stwo = Singleton.INSTANCE; } public void testSerialize() { logger.info("testing singleton serialization..."); [b] writeSingleton(); Singleton s1 = readSingleton(); Singleton s2 = readSingleton(); Assert.assertEquals(true, s1 == s2);[/b] } private void writeSingleton() { try { FileOutputStream fos = new FileOutputStream("serializedSingleton"); ObjectOutputStream oos = new ObjectOutputStream(fos); Singleton s = Singleton.INSTANCE; oos.writeObject(Singleton.INSTANCE); oos.flush(); } catch(NotSerializableException se) { logger.fatal("Not Serializable Exception: " + se.getMessage()); } catch(IOException iox) { logger.fatal("IO Exception: " + iox.getMessage()); } } private Singleton readSingleton() { Singleton s = null; try { FileInputStream fis = new FileInputStream("serializedSingleton"); ObjectInputStream ois = new ObjectInputStream(fis); s = (Singleton)ois.readObject(); } catch(ClassNotFoundException cnf) { logger.fatal("Class Not Found Exception: " + cnf.getMessage()); } catch(NotSerializableException se) { logger.fatal("Not Serializable Exception: " + se.getMessage()); } catch(IOException iox) { logger.fatal("IO Exception: " + iox.getMessage()); } return s; } public void testUnique() { logger.info("testing singleton uniqueness..."); Singleton another = new Singleton(); logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }
前面這個測試案例序列化例12中的單例類,并且兩次重構它。然后這個測試案例檢查看是否被重構的單例類實例是同一個對象。下面是測試案例的輸出:
- Buildfile:build.xml
- init:
- [echo]Build 20030422 ( 22 - 04 - 2003 11 : 32 )
- compile:
- run-test-text:
- [java].INFOmain:testingsingletonserialization...
- [java].INFOmain:testingsingletonuniqueness...
- [java]INFOmain:checkingsingletons for equality
- [java]Time: 0.1
- [java]OK( 2 tests)
Buildfile: build.xml init: [echo] Build 20030422 (22-04-2003 11:32) compile: run-test-text: [java] .INFO main: testing singleton serialization... [java] .INFO main: testing singleton uniqueness... [java] INFO main: checking singletons for equality [java] Time: 0.1 [java] OK (2 tests)
單例模式結束語
單例模式簡單卻容易讓人迷惑,特別是對于Java的開發者來說。在這篇文章中,作者演示了Java開發者在顧及多線程、類載入器和序列化情況如何實現單例模式。作者也展示了你怎樣才能實現一個單例類的注冊表,以便能夠在運行期指定單例類。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
