`
lc52520
  • 浏览: 369021 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

单例模式完全剖析【Z】

    博客分类:
  • java
阅读更多
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)  
 

概要
单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在本月的专栏中,David Geary探讨了单例模式以及在面对多线程(multithreading)、类装载器(classloaders)和序列化 (serialization)时如何处理这些缺陷。

单例模式适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统,它们都是原型的例子。典型的情况是,那些对象的类型被遍及一个 软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。
单例模式的用意在于前一段中所关心的。通过单例模式你可以:


确保一个类只有一个实例被建立
提供了一个对对象的全局访问指针
在不影响单例类的客户端的情况下允许将来有多个实例

尽管单例设计模式如在下面的图中的所显示的一样是最简单的设计模式,但对于粗心的Java开发者来说却呈现出许多缺陷。这篇文章讨论了单例模式并揭示了那些缺陷。
注意:你可以从Resources 下载这篇文章的源代码。

单例模式

在《设计模式》一书中,作者这样来叙述单例模式的:确保一个类只有一个实例并提供一个对它的全局访问指针。
下图说明了单例模式的类图。
(图1)

单例模式的类图

正如你在上图中所看到的,这不是单例模式的完整部分。此图中单例类保持了一个对唯一的单例实例的静态引用,并且会从静态getInstance()方法中返回对那个实例的引用。
例1显示了一个经典的单例模式的实现。
例1.经典的单例模式

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

 

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() {    
   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 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); 
   } 
} 

 

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

 

if(instance == null) {   
    instance = new Singleton();   
}  
 



如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情 况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法 测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类:
例4.人为安排的方式

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");   
     }   
  }   
}
  
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 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); 
      } 
   } 
} 

 

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 ==  null throw   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); 
   } 
} 
分享到:
评论

相关推荐

    15.设计模式面试真题-31页.7z

    设计模式通常分为三大类:创建型模式(如工厂模式、抽象工厂模式、单例模式、建造者模式和原型模式)、结构型模式(如代理模式、装饰器模式、适配器模式、桥接模式、组合模式、外观模式和享元模式)和行为型模式(如...

    软件设计模式实训.7z

    培养学生掌握简单工厂模式、工厂方法模式、单例模式、原型模式、桥接模式、适配器模式、装饰模式、代理模式、命令模式、观察者模式等常用设计模式的设计思想,并掌握在实际软件工程领域中的运用,能够把所学的原理...

    研磨设计模式-配套源代码.7z

    2. 单例模式:保证一个类只有一个实例,并提供一个全局访问点。在多线程环境下,单例模式能确保安全初始化。 3. 观察者模式:定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会...

    MyDemo-qt-singleton.7z

    通过分析和理解这部分代码,开发者可以学习到如何在多线程环境中有效地应用单例模式,以及如何处理与静态数据和内存管理相关的细节。 总之,"MyDemo-qt-singleton"项目是一个实用的学习资源,帮助开发者了解如何在...

    易语言禁止程序重复运行源码.7z

    标题提到的“易语言禁止程序重复运行源码”是一个易语言编写的程序,其主要功能是防止同一程序在同一台计算机上多次启动,即实现单例模式。 在计算机编程中,单例模式是一种常见的设计模式,它的目的是控制类的实例...

    J2EE核心模式.7z

    - **单例Session Bean**:在高并发环境下,为避免创建过多的Session Bean实例,可以使用单例模式确保只有一个实例存在。 - **前端控制器**:通过一个中心控制组件处理所有用户请求,简化请求处理逻辑,提高代码复用...

    labview程序设计模式入门精通宝典开发与实战开发200例.7z

    通过学习这本书,读者可以了解到如何在LabVIEW环境中应用这些模式,例如“工厂模式”用于动态创建对象,“观察者模式”用于事件驱动编程,以及“单例模式”确保类只有一个实例等。 书中的200个实战开发例子将涵盖...

    李建忠 C#面向对象设计模式纵横谈(25):设计模式总结

    例如,在C#中,可以利用静态类实现单例模式,确保类只有一个实例;工厂方法则通过接口或抽象类定义创建对象的接口,而具体实现交给子类决定。 2. 结构型模式:这类模式关注如何组合和包装现有的类和对象,包括...

    GCDSingleTest Demo代码

    通过运行和分析这个Demo,你可以深入理解GCD和单例模式在iOS开发中的应用,从而提升代码的可维护性和性能。 总的来说,"GCDSingleTest Demo"是一个学习和实践GCD与单例模式的好资源。它将帮助开发者更好地掌握如何...

    ComDemo.7z

    在"ComDemo"中,可能包含对这些设计模式的实现,例如,使用单例模式确保在整个程序中只有一个特定类的实例,或者使用工厂模式动态创建对象,以提高代码的灵活性。 串口通信是"ComDemo"项目中的一个重要部分,这是...

    .NET应用程序架构设计 原则 模式与实践 源码

    3. **设计模式**:书中提到的模式可能包括工厂模式、单例模式、建造者模式、观察者模式、装饰器模式、代理模式、策略模式、状态模式、命令模式等。这些设计模式是解决软件设计中常见问题的成熟解决方案,是软件工程...

    上海交通大学程序设计ppt.7z

    11. **软件设计模式**:可能会介绍一些常见的设计模式,如工厂模式、单例模式、装饰器模式等,帮助学生理解如何构造可复用、可扩展的代码。 通过这份上海交通大学的程序设计PPT,学生不仅可以学习到理论知识,还能...

    华科Java实验报告.7z

    12. **设计模式**:单例模式、工厂模式、观察者模式等常见设计模式的理解和应用。 这份实验报告将帮助读者深入理解Java编程语言,通过实际操作加深理论知识的掌握,提高编程能力和问题解决能力。同时,对于后续的...

    面试题.7z

    - 设计模式:工厂模式、单例模式、装饰器模式、观察者模式等23种经典设计模式的应用和理解。 - 微服务架构:服务拆分、API Gateway、服务发现、容错机制等。 - 云计算:AWS、Azure、Google Cloud等云平台的使用,...

    hanmingcode.7z

    8. 软件设计模式:理解并应用常见的设计模式,如工厂模式、单例模式、观察者模式等。 为了充分利用"hanmingcode.7z"中的内容,首先需要使用7-Zip或类似的解压缩工具将其解压。然后,根据文件结构和内容,你可以阅读...

    程序设计.7z

    同时,理解设计模式也是提高编程能力的关键,如单例模式、工厂模式、观察者模式等,这些模式为解决常见问题提供了标准的解决方案。 在题库方面,可能包含了编程基础练习、算法题目、数据结构实战以及特定编程语言的...

    软件工程配套习题.7z

    2. 设计模式与体系结构:习题可能包含设计模式的选择和应用,如工厂模式、单例模式、观察者模式等,以及不同软件体系结构(如三层架构、微服务架构)的理解和设计。 3. 系统分析:学生可能会被要求进行可行性研究,...

    CppCon2015-master.7z

    5. **设计模式与最佳实践**:设计模式是软件工程中的经典话题,大会可能探讨了C++中的一些经典设计模式,如工厂模式、单例模式、观察者模式等,并结合实际问题讨论了如何在C++中选择和实现设计模式。 6. **其他主题...

    软件工程PPT课件.7z

    4. **软件设计模式**:设计模式是解决常见问题的模板,如工厂模式、单例模式、观察者模式等。理解并应用设计模式可以提升软件的可复用性和可扩展性。 5. **编程规范与版本控制**:编码阶段强调遵循编程规范,提高...

    东北大学JAVA面向对象实验全部实验代码1到6.7z.zip

    8. **设计模式**:如单例模式、工厂模式、观察者模式等,这些都是在实际开发中常用的面向对象设计模式。 通过这些实验,学生不仅可以提升编程技巧,还能掌握如何用面向对象的思想去分析和解决问题,为未来从事软件...

Global site tag (gtag.js) - Google Analytics