传统懒汉式的单例模式在多线程中存在的一些问题
- 存在线程安全问题
- 不能保证单例
DCL模式的单例
public class Singleton {
// 类的内部创建对象
public static volatile Singleton singleton;
// 构造器私有化
private Singleton() {};
// 对外暴露获取对象方法
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
笔记
- 类内部对象中为何使用volatile关键字进行修饰?
此处使用的是volatile特性中的
禁止指令重排,保证线程安全。
- 为什么还会存在线程安全问题?
原因在于某一个线程执行到第一次检测,读取到的
singleton不为null时,singleton的引用对象可能还没有完成初始化。 singleton = new Singleton();可以分为一下三步完成(通过字节码得到的伪代码) 字节码可以通过cmd进入Java文件所在目录,运行命令javac ./xxx.java和javap -c ./xxx.class获取字节码信息。
memory = allocate(); // 1.分配对象内存空间
singleton(memory); // 2.初始化对象
singleton = memory; // 3.设置singleton指向刚分配的内存地址,此时singleton != null
因为,上述步骤2和3不存在数据依赖关系,因此可以重排为以下情形:
memory = allocate(); // 1.分配对象内存空间
singleton = memory; // 3.设置singleton指向刚分配的内存地址,此时singleton != null,但是对象还没有完成初始化!!!
singleton(memory); // 2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问singleton不为null时(第一次判断为空地方),由于singleton实例未必已经完成初始化就被直接返回,也就造成了线程安全问题。
因此singleton要使用volatile进行修饰,禁止创建对象时出现指令重排现象。
- 为什么两次判断为空?
- 第一次判断为空:为了提高代码的执行效率,由于单例模式中只需要创建一次实例,所以当创建完成之后,再次调用时无需进入相同的代码块。
- 第二次判断为空:防止二次创建实例。 可能会存在以下情形:<1>t1线程执行代码,正常通过两次校验,正准备创建Singleton实例 <2>系统资源被t2线程抢夺,也正常通过两次校验(因为此时t1线程还未创建实例),并创建了Singleton实例 <3>此时t1重新获取系统资源后,通过第二个判空发现已经存在实例则不会再重新创建,而是直接返回
- 使用synchronized
- 为了保证某一时刻只有一个线程进行操作,保证了线程安全。
技术有限,定有不合理之处,欢迎指正并讨论。