概述
前言: 之前看到介绍单例模式的一种线程安全的模式,后面看并发书籍时发现这个线程安全的模式如果修改一下则会有一些隐患,故记录下来。
参考书籍:《Java并发编程的艺术》
先看下这个单例吧
双重校验锁先判断 uniqueInstance 是否已经被初始化了,如果没有被实例化,那么才对实例化语句进行加锁。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
将其修改为下面的版本(将uniqueInstance前的volatile关键字删除了):
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
然后介绍下问题
首先说下双重检查锁定的目的,第一个是延迟初始化来降低初始化类和创建对象的开销,第二个保障线程安全的同时降低锁的使用(不为null直接返回不用加同步块)。但是第二个似乎不成功…
我更改后的代码第一眼看去,很正常,不会出现线程安全问题:如果第一次检查uniqueInstance != null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅度降低synchronized带来的性能开销。这个理解似乎看起来两全其美。但是这是一个错误的优化!在线程执行到if (uniqueInstance == null)
这里时,uniqueInstance 引用的对象有可能还没有完成初始化。
问题的根源
前面的双重检查锁定示例代码的这一行uniqueInstance = new Singleton();
创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
uniqueInstance = memory; // 3.设置uniqueInstance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序。2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1.分配对象的内存空间
uniqueInstance = memory; // 3.设置uniqueInstance指向刚分配的内存地址
// 注意:此时对象还没有被初始化
ctorInstance(memory); // 2.初始化对象
至于为什么这里可能发生重排序,原因在于2和3重排序后并没有改变单线程程序执行的结果,可以提高程序的执行性能。具体的重排序原因不过多赘述,自查。
然后在uniqueInstance = new Singleton();
中发生了重排序后,第一个线程执行到了uniqueInstance = memory
,此时还没有初始化,但是对象已经生成了,第二个线程在if (uniqueInstance == null)
这行进行判断发现不为null,直接执行到了return uniqueInstance;
这一行,导致返回一个未初始化的对象。
解决方案
基于volatile的解决方案
直接在uniqueInstance前加一个volatile关键字,就可以实现线程安全的延迟初始化,即我未修改的那份代码。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
声明为volatile后,2与3之间的重排序在多线程环境下将会被禁止,这样就保证了线程安全的延迟初始化。
注意:
volatile的语义第一个是保证可见性,第二个是禁止指令重排序优化。而volatile屏蔽指令重排序的语义在JDK 5中才被完全修复,这也意味着JDK 5版本前的这个解决方案是行不通的。
(这个解决方案需要JDK5以上版本,因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义。)
基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且在线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性,可以实现另一种线程安全的延迟初始化方案。
public class Singleton {
private static class InstanceHolder {
public static Singleton uniqueInstance = new Singleton();
}
private Singleton() {}
public static Singleton getUniqueInstance() {
return InstanceHolder.uniqueInstance; // 这里会导致InstanceHolder类被初始化
}
}
这个方案的实质是:允许之前说的3和2进行重排序,但不允许非构造线程“看到”这个重排序。即只有第一个拿到这个初始化锁的线程才能看到这个重排序,其他后面获取到初始化锁的线程看不到这个重排序过程。
关于类初始化这方面具体的解释可以查看《Java并发编程的艺术》的第3章Java内存模型的3.8节
最后
以上就是鳗鱼野狼为你收集整理的谈一谈多线程中的双重检查锁定的全部内容,希望文章能够帮你解决谈一谈多线程中的双重检查锁定所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复