第003弹:懒汉型单例模式的演变
这篇文章主要是为了从头开始,详细介绍懒汉模式的实现,以及实现的原因。
之前写过一篇比较浅的懒汉模式,可以优先参照:设计模式(一)单例模式:2-懒汉模式
Step1:基础的懒汉模式
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
基础的懒汉模式保证了在调用 getInstance() 方法的时候才第一次初始化单例对象。
但是这么做无法保证在多线程环境下只创建一个对象。
显然,假设有多个线程同时调用 getInstance() 方法,在第一个线程执行完毕之前,会有多个 LazyInstance 对象被创建。
Step2:为 getInstance() 方法加上同步锁
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
通过简单地在方法上加上同步锁,可以保证同时只有一个线程调用这个静态方法,从而保证在多线程环境下的单例。
然而这么做有明显的 performance 隐患。
假设有多个线程想要获取 instance,无论此时对象是否已经被创建,都要频繁地获取锁,释放锁。这种做法很影响效率。
Step3:在 getInstance() 方法内部增加同步代码块
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; } }
既然在方法上加同步锁不合适,那么就在方法内部增加同步代码块。
在判断 instance == null 之后,增加的同步代码块就不会产生 performance 问题,因为之后的访问会直接 return,不会进入同步代码块。
但是这么做,不能完整地保证单例。
参照 Step1,假设有多线程调用,且都通过了 instance == null 的判断,那么一样会有多个 LazySingleton 对象被创建。
Step4:使用 Double-Checked Locking
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
通过增加双重判断,以及同步代码块,就可以避免 Step3 中可能出现的隐患。
但是 Double-Checked Locking 虽然能够保证单例的创建,但是在多线程的情况下可能出现某个线程使用创建不完全的对象的情况。
Step5:使用 volatile 关键字修饰字段 instance
public class LazySingleton { private static volatile LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
参考文档:The "Double-Checked Locking is Broken" Declaration
如果不适应英文描述,ImportNew 对这篇文档进行了翻译:可以不要再使用Double-Checked Locking了
这里面讲述了 Double-Checked Locking 在懒汉模式下可能出现的问题。
主要问题在于 Java 指令重排。
当 Java 代码被编译器翻译成字节码被存储在 JVM 时,为了提高性能,编译器会对这些操作指令进行指令重排。
也就是说,代码在计算机上执行的顺序,会被打乱。
返回到本例的问题,懒汉模式最关键的2个操作:
- 在 heap 中创建一个 LazyInstance 对象。
- 为字段 instance 赋值。
假设操作1在操作2之前被执行,那么代码就没有问题。
反之若操作2在操作1之前被执行,如果不能保证创建 LazyInstance 对象的过程是原子的,那么代码还是会出现问题,因为 instance 指向了一个没有被创建完全的对象。
事实上,引用类型和64位类型(long 和 double)都不能被原子地读写。
解决方案是通过 volatile 关键字来禁止指令重排(这是 volatile 的两个作用之一,另一个作用是保证共享变量的可见性,这里不深入展开)