转载自:http://blog.youkuaiyun.com/songylwq/article/details/6058771
概要
单例模式是最简单的设计模式之一,但是对于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;
- }
- }
在例1中的单例模式的实现很容易理解。ClassicSingleton类保持了一个对单独的单例实例的静态引用,并且从静态方法getInstance()中返回那个引用。
关于ClassicSingleton类,有几个让我们感兴趣的地方。首先,ClassicSingleton使用了一个众所周知的懒汉式实例化去创建那个单例类的引用;结果,这个单例类的实例直到getInstance()方法被第一次调用时才被创建。这种技巧可以确保单例类的实例只有在需要时才被建立出来。其次,注意ClassicSingleton实现了一个protected的构造方法,这样客户端不能直接实例化一个ClassicSingleton类的实例。然而,你会惊奇的发现下面的代码完全合法:
正如前面的清单所示,例2的简单测试顺利通过----通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。下面的部分着重于用多线程测试单例类。
多线程因素的考虑
在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
- 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()方法:
- 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 Singleton getInstance() {
- if(singleton == null) {
- synchronized(Singleton.class) {
- singleton = new Singleton();
- }
- }
- return singleton;
- }
这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。
双重加锁检查
初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术: