参考:<<Java并发编程的艺术>>
单例模式有以下特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。
1. 饿汉式
在类初始化时直接实例化,没有线程安全问题,简单粗暴。
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
2. 懒汉式
我们有时候需要采用延迟初始化来降低初始化类和创建对象的开销。一般采用双重检查锁定来延迟初始化,实现线程安全的延迟初始化需要一些技巧。
2.1 同步synchronized
对getInstance()方法做了同步处理,synchronized将会导致性能开销。如果该方法被多个线程频繁调用,将会导致程序执行性能下降;如果没有被多个线程频繁调用,那么这种方式也是可行的。
public class SyncSingleton {
private static SyncSingleton instance;
private SyncSingleton() {
}
public static synchronized SyncSingleton getInstance() {
if (instance == null) {
instance = new SyncSingleton();
}
return instance;
}
}
2.2 多重检查锁定
在JDK1.6之前的JVM中,synchronized存在巨大的性能开销。因此,出现了双重检查锁定(Dobule-Checked- Locking),想通过这种方式来降低同步的开销。示例:
public class DCLSingleton {
private static DCLSingleton instance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (instance == null) { // 1
synchronized (DCLSingleton.class) {// 2
if (instance == null) { // 3
instance = new DCLSingleton();// 4可能会有问题
}
}
}
return instance;
}
}
上面的代码表面看起来没有问题:
- 多个线程调用时,通过加锁来保证只有一个线程能创建对象
- 在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回创建好的对象
实际上在执行第1行时,发现instance不为null,但是instance引用的对象有可能还没有完成初始化。因为第4行实例化可以分解为以下3行伪代码:
上面的2和3之间,可能会被重排序。2和3重排序之后执行顺序如下:
这种情况在单线程环境下不会有问题,并不影响最终的结果;但是在多线程环境下,就有可能instance非空,但还没初始化完成。
在知道原因之后,可以有两种方法实现线程安全的延迟初始化:
- 不允许2和3重排序
- 允许2和3重排序,但不允许其他线程“看到”这个重排序
(1)基于volatile关键字
上面示例中,只需要用volatile修饰instance,不允许2和3重排序,就可以实现线程安全的延迟初始化。
注:需要使用JDK5及以上版本,因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义(禁止指令重排序)
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
(2)基于类初始化
JVM在类的初始化阶段(即Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM回去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性,可以实现另一种线程安全的延迟初始化方案,称为Initialization On Demand Holder idiom。示例:
public class ClassloadSingleton {
private ClassloadSingleton() {
}
private static class InstanceHolder {
public static ClassloadSingleton instance = new ClassloadSingleton();
}
public static ClassloadSingleton getInstance() {
return InstanceHolder.instance;
}
}
这个方案利用类初始化阶段有锁的特点,即使在实例化过程中发生了重排序,也不会导致初始化前被其他线程拿到引用,因为其他线程无法获取锁只能等待,直到持有锁的线程初始化完成。这就是允许2和3重排序,但不允许其他线程“看到”这个重排序。
具体的初始化过程可以参考书中p73-78说明。