聊一聊开发中单例模式的双层检验锁
单例模式懒汉式普遍写法
public class SingleInstance {
private static SingleInstance instance;
public static synchronized SingleInstance getInstance() {
if (instance == null) {
instance = new SingleInstance ();
}
return instance;
}
}
上面例子为懒汉式写法,看似没有问题,实则存在很大的bug
因此就有了双层检验锁,先上代码,后分析原理
双重验证锁
public class SingleInstance {
private static volatile SingleInstance instance;
public static SingleInstance getInstance() {
//第一次校验
if (instance == null) {
synchronized (SingleInstance.class) {
//第二次校验
if (instance == null) {
instance = new SingleInstance();
}
}
}
return instance;
}
}
那么为什么需要两次空判断呢?
第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance()方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的普通懒汉模式没什么区别,每次都要去竞争锁。
第二次校验:如果没有第二次校验,假设线程A执行了第一次校验后,判断为null,这时线程B也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来线程B获得锁,创建实例。这时线程A又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
接下来分析程序执行过程,看下是否完美
程序执行过程
双重校验锁方式的执行过程如下:
- 线程A进入 getInstance() 方法。
- 由于第一次校验 instance为 null,线程A向下执行进入 synchronized 块中。
- 此时线程B也进入 getInstance() 方法。
- 由于线程A还处于synchronized 块中未执行完成, instance仍然为 null,线程B向下执行试图向下执行进入synchronized 块中。然而,由于线程A已经持有该锁,线程B在锁外阻塞等待。
- 线程A执行,由于在 第二次校验 处实例为 null,线程A创建一个 SingleInstance 对象并将其引用赋值给 instance。
- 线程A退出 synchronized 块并从 getInstance() 方法返回实例。
- 线程B获取锁向下执行 进入synchronized 块中并检查 instance 是否为 null。
- 由于 instance是非 null 的,并没有创建第二个 SingleInstance 对象,由线程A所创建的对象被返回。
看看执行流程,非常完美,似乎毫无破绽,可是
存在新的问题
以上双重检查锁执行流程理论是完美的。可是很不幸。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
instance = new SingleInstance();
该语句非原子操作,实际是三个步骤。
- 给instance 分配内存;
- 调用 SingleInstance 的构造函数来初始化成员变量;
- 将给instance 对象指向分配的内存空间(此时instance 才不为null);
虚拟机的指令重排序
执行命令时虚拟机可能会对以上3个步骤进行不规律的执行顺序 最后可能是132这种 分配内存并修改指针后未初始化 多线程获取时可能会出现问题。
当线程A进入同步方法执行instance = new SingleInstance(); 代码时,恰好这三个步骤重排序后为1 3 2;
那么步骤3执行后 instance 已经不为null ,但是未执行步骤2,instance 对象初始化不完全,此时线程B执行getInstance() 方法,第一步判断时 instance 不为null,则直接将未完全初始化的 instance 对象返回了。
那么如何解决呢
如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的,同时还会禁止指令重排序
所以使用volatile关键字会禁止指令重排序,可以避免这种问题。使用volatile关键字后使得 instance = new SingleInstance();语句一定会按照上面拆分的步骤123来执行。