`

单例模式

    博客分类:
  • Java
 
阅读更多

在《设计模式》一书中,作者这样来叙述单例模式的:确保一个类只有一个实例并提供一个对它的全局访问指针。
下图说明了单例模式的类图。
(图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. }   
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类的实例。然而,你会惊奇的发现下面的代码完全合法:

Java代码 复制代码 收藏代码
  1. public class SingletonInstantiator {     
  2.   public SingletonInstantiator() {     
  3.    ClassicSingleton instance = ClassicSingleton.getInstance();    
  4. ClassicSingleton anotherInstance =    
  5. new ClassicSingleton();    
  6.        ...     
  7.   }     
  8. }   
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.一个单例模式的案例

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. }   
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.是这个测试案例的输出

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)  
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()方法由于下面的代码而不是线程安全的:

Java代码 复制代码 收藏代码
  1. 1if(instance == null) {    
  2. 2:    instance = new Singleton();    
  3. 3: }   
1: if(instance == null) { 
2:    instance = new Singleton(); 
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. }   
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.失败的测试

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. }   
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的输出

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)   
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()方法:

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. }   
public synchronized static Singleton getInstance() { 
   if(singleton == null) { 
      simulateRandomActivity(); 
      singleton = new Singleton(); 
   } 
   logger.info("created singleton: " + singleton); 
   return singleton; 
} 


在同步化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)   
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()方法:

Java代码 复制代码 收藏代码
  1. public static Singleton getInstance() {    
  2.    if(singleton == null) {    
  3.       synchronized(Singleton.class) {     
  4.          singleton = new Singleton();    
  5.       }    
  6.    }    
  7.    return singleton;    
  8. }   
public static Singleton getInstance() { 
   if(singleton == null) { 
      synchronized(Singleton.class) {  
         singleton = new Singleton(); 
      } 
   } 
   return singleton; 
} 



这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程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. }   
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.一个简单的单例类

Java代码 复制代码 收藏代码
  1. public class Singleton {    
  2.    public final static Singleton INSTANCE = new Singleton();    
  3.    private Singleton() {    
  4.          // Exists only to defeat instantiation.    
  5.       }    
  6. }   
public class Singleton { 
   public final static Singleton INSTANCE = new Singleton(); 
   private Singleton() { 
         // Exists only to defeat instantiation. 
      } 
} 



这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现;你应该这样使用它:

Java代码 复制代码 收藏代码
  1. Singleton singleton = Singleton.INSTANCE;    
  2. singleton.dothis();    
  3. singleton.dothat();    
  4. ...   
      Singleton singleton = Singleton.INSTANCE; 
      singleton.dothis(); 
      singleton.dothat(); 
      ... 



当然万事并不完美,前面的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. }   
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 使用反射实例化单例类

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

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

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. }   
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容器--所以不管你在实现你的单例类时是多么小心你都最终可以得到多个单例类的实例。如果你想要确保你的单例类只被同一个的类载入器装入,那你就必须自己指定这个类载入器;例如:

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. }   
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 一个可序列化的单例类

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.    }   
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 测试一个可序列化的单例类

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. }   
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中的单例类,并且两次重构它。然后这个测试案例检查看是否被重构的单例类实例是同一个对象。下面是测试案例的输出:

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)   
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开发者在顾及多线程、类载入器和序列化情况如何实现单例模式。作者也展示了你怎样才能实现一个单例类的注册表,以便能够在运行期指定单例类。

分享到:
评论

相关推荐

    43丨单例模式(下):如何设计实现一个集群环境下的分布式单例模式?1

    单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点。在单例模式中,类的构造函数是私有的,防止外部直接创建对象,而是通过静态方法获取该类的唯一实例。单例模式的唯一性通常是在进程范围内,...

    设计模式单例模式和工厂模式综合应用

    "设计模式单例模式和工厂模式综合应用"的主题聚焦于两种常用的设计模式:单例模式和工厂模式,并探讨它们如何协同工作来实现高效、灵活的代码结构。这个主题尤其适用于Java编程语言,因为Java的面向对象特性使得设计...

    设计模式之单例模式(结合工厂模式)

    单例模式是软件设计模式中的一种经典模式,它保证了类只有一个实例存在,并提供一个全局访问点。在Java等面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。结合工厂模式,...

    java单例模式实例

    单例模式是软件设计模式中的一种经典模式,用于确保一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方法,每种都有其特点和适用场景。接下来,我们将深入探讨这些实现方式。 首先,我们...

    使用C++11实现线程安全的单例模式

    在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。线程安全的单例模式在多线程环境下尤其重要,因为不正确的实现可能导致多个线程创建多个实例,这违反了单例模式...

    设计模式——单例模式

    **设计模式——单例模式** 在软件工程中,设计模式是一种在特定场景下解决常见问题的标准方案,可以被复用并提升代码质量。单例模式是设计模式中的一种,它保证一个类只有一个实例,并提供一个全局访问点。这种模式...

    C#单例模式详解 C#单例模式详解C#单例模式详解

    单例模式是软件设计模式中的一种,它保证一个类只有一个实例,并提供一个全局访问点。在C#中,单例模式常用于管理共享资源或控制类的实例化过程,以提高性能、节约系统资源,特别是在整个应用程序生命周期内只需要一...

    7种单例模式

    单例模式是软件设计模式中的一种经典模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下非常有用,比如控制共享资源、管理配置对象等。下面将详细介绍七种常见的单例模式实现...

    使用单例模式实现计数器

    其中,单例模式是一种非常经典且常用的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。在C#中,我们可以利用单例模式来创建一个计数器类,以确保在整个应用程序的生命周期内,计数器只...

    使用单例模式创建学生管理系统(饿汉式、懒汉式)

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。在Java或类似的面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。在这个...

    c++单例模式线程日志类

    在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。在这个特定的场景中,我们讨论的是一个实现了单例模式的日志类,该类专为多线程环境设计,具备日志等级控制、...

    单例模式实现mdi界面子窗体控制

    首先向关注过我这个系列...这立刻让我想到了最常用也是最简单最容易理解的一个设计模式 单例模式 何为 单例模式 ? 故名思议 即 让 类 永远都只能有一个实例。 由于 示例代码 比较简单 我也加了注释,这里就不在赘述

    几种单例模式demo

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,比如控制资源的唯一性、管理共享配置或者创建昂贵的对象时避免频繁创建销毁。 ...

    Java 单例模式 工具类

    Java中的单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供全局访问点。在Java编程中,单例模式常用于控制资源的访问,比如数据库连接池、线程池或者日志对象等。本篇文章将深入探讨如何在Java中...

    单例模式详解~~单例模式详解~~

    单例模式是一种设计模式,它的主要目标是确保一个类只有一个实例,并提供一个全局访问点。在软件工程中,单例模式常用于控制资源的共享,比如数据库连接池、线程池或者日志系统等,这些资源通常需要全局唯一且高效地...

    单例模式(singleton)

    单例模式是软件设计模式中的一种,它的核心思想是确保一个类在整个系统中只有一个实例,并提供一个全局访问点。在Java或类似编程语言中,单例模式常常被用来管理资源,比如数据库连接、线程池或者配置信息,因为这些...

    Qt qml Singleton 单例模式

    在Qt的Qml环境中,单例模式是一种设计模式,它允许在整个应用程序中创建一个全局访问点,确保某个类只有一个实例存在。这样的设计模式在需要共享数据或者服务时非常有用,避免了多处创建相同对象导致的数据不一致或...

    设计模式之单例模式源码demo

    单例模式是软件设计模式中的经典模式之一,其主要目的是控制类的实例化过程,确保在应用程序的整个生命周期中,某个类只有一个实例存在。这样的设计通常适用于那些需要频繁创建和销毁,但资源消耗较大的对象,如...

    23钟设计模式之单例模式

    单例模式是一种常用的设计模式,它的核心思想是在整个应用程序中,一个类只能有一个实例存在。单例模式常用于控制资源的共享,例如数据库连接池、日志服务等。单例模式有多种实现方式,常见的包括懒汉式、饿汉式以及...

Global site tag (gtag.js) - Google Analytics