概述
单例模式可以说是所有设计模式中最简单的,因为它的类图是如此的一目了然。但同时,它可能也是所有设计模式中最为复杂的,因为它的实现方式多种多样,特定的实现方式只能适应特定的场合。
各种实现的异同
虽然该模式实现方式多种多样,但是其实现还是有一些关键共通点的。如下:
- 构造器私有
- 提供一个返回实例的类方法
而各种实现不同点主要集中在实例的创建时机和方式上。
实现
最简单的实现
1 | public class Singleton |
当不涉及多线程时,上面的代码通常是可行的。但在多线程执行时,就可能会出现问题。比如假设场景如下:
有两个线程同时调用 getInstance()
方法,线程一执行完第 7 行代码,即判断完成后,JVM 将 CPU 切换到线程二,由于线程一还没有执行第 9 行代码, 即 instance
还是 null
,所以线程二创建了实例。当 CPU 切换回线程一时,线程一会继续第 9 行代码,又创建了一个实例。
这样,问题就出现了——这个单例类就不再是单例了——在多线程中这种实现不适用。
同步方法
1 | public class Singleton |
获取单例的方法被修改为同步方法了,这样在多线程使用就不再有问题了。
但是,这种实现可能导致性能问题。因为,对于 getInstance()
方法而言,除了第一次创建实例外,其后的调用都仅仅返回已创建的实例(耗时其实很少), 每次调用都进行耗时的方法同步其实是不必要的。
所以,这种写法虽然可以解决多线程的问题,但同时引入了性能问题。
双重检查加锁(DCL)
1 | public class Singleton |
双重检查加锁(DCL,Double-Checked Locking),这样写,貌似就同时解决了并发和同步的问题。 方法仅在第一次创建实例时进入同步块,而之后统统在第一个判断时返回了。
上面这段程序,从源代码级别上来看是看不出什么问题的。问题出在第 13 行,看起来 instance = new Singleton();
只是一句代码, 但实际上它不是一个原子操作(事实上高级语言中非原子操作很多)。查看这句代码在被编译后在 JVM 执行的对应汇编代码会发现, 它被编译成了 8 条汇编指令,大致做了 3 件事:
- 给实例分配内存;
- 初始化构造器;
- 将
instance
对象指向分配的内存空间(注意到这步instance
就非null
)
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及 JDK 1.5 之前 JMM(Java Memory Model)中 Cache、寄存器到主内存回写顺序的规定, 上面第 2 点和第 3 点的顺序是无法保证的,即执行顺序可能是 1-2-3,也可能是 1-3-2。
如果线程一中执行顺序是 1-3-2,当执行完 3 未执行 2 之前,被切换到线程二上,这时 instance 因为已经在线程一内执行过第 3 点,因而是非空了, 所以线程二直接拿走instance,然后使用、报错。
所以,DCL 这种“教科书式的范例”代码实际上是不完全正确的。的确,在一些语言(如 C 语言)上 DCL 是可行的,这取决于是否能保证 2、3 步的顺序。
在 JDK1.5 之后,官方已经注意到这种问题,因此调整了JMM、具体化了 volatile
关键字。因此,如果 JDK 是 1.5 或更高版本, 只需要将 instance
的定义改成 private volatile static Singleton instance = null;
就可以保证每次 instance
都从主内存读取,就可以使用 DCL 的写法来完成单例模式。
当然,volatile
或多或少会影响性能,最重要的是还要考虑老版本的 JDK。
饿汉式
1 | public class Singleton |
根据 JLS(Java Language Specification)中的规定,一个类在一个 ClassLoader
中只会被初始化一次,这点是 JVM 本身保证的。所以,这种写法基本上是完美的,不存在并发问题。
但是它是饿汉式的,在 ClassLoader
加载了 Singleton
类后,实例便第一时间被创建。这可能使得这种写法在某些场景中不适用,比如实例创建依赖参数或配置文件; 或者实例会使用比较稀缺的资源,但是真正使用得很少,所以希望“懒加载”。
内部类持有实例
1 | public class Singleton |
Lazy initialization holder class,使用 JVM 本身机制保证线程安全问题;并且它还是懒汉式的;同时读取实例时不会进行同步,没有性能问题;也不依赖 JDK 版本。
可以说这种写法基本上算是“完美”、“无可挑剔”的。
确保唯一
以上所有写法我们重点讨论的是它们的线程安全性以及性能问题,而都“故意”忽略了一方面的问题。 仔细观察我们可以发现,以上写法“最多”确保我们通过 getInstance()
方法可以获取到同一个实例,但是并没有阻止其它任何形式来创建实例。
因此,要确保类只能创建单一实例,还要屏蔽外部对类实例的创建。
我们知道,创建实例的方法通常有以下几种:
- 直接
new
创建; - 反射创建;
- 反序列化创建。
对于第 1 种情况,通常应该把单例类的构造器限制为 private
或 protected
的,普遍来说都会限制为最严格的 private
。 然而,即使将构造器限制为 private
的,也仅仅只能阻止普通的 new
创建对象。而反射可以使用 setAccessible()
方法突破 private
的限制。 对此,还需要在 ReflectPermission("suppressAccessChecks")
权限下使用安全管理器(SecurityManager)的 checkPermission()
方法来限制这种突破。
另外,如果类实现了 Serializable
接口,则可以通过反序列化来创建对象,则也可能导致实例不唯一。对此,应当同时实现 readResolve()
方法来保证反序列化时也能得到唯一对象。
综上,一个较完善的单例模式应该类似下面这样:
1 | public class Singleton implements Serializable |
总结
单例的实现方法很多,不限于上述几种,如使用本地线程(ThreadLocal
)来处理并发以及保证一个线程内一个单例的实现、GoF 原始例子中使用注册方式应对单例类需要继承时的实现、 使用指定类加载器去应对多 ClassLoader
环境下的实现等等。
在选择单例实现时,应该考虑应用场景,而非一味追求完美避免过度的复杂设计。