Java中多线程环境下DCL单例模式小笔记

151 阅读2分钟

传统懒汉式的单例模式在多线程中存在的一些问题

  • 存在线程安全问题
  • 不能保证单例

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;
    }
}

笔记

  1. 类内部对象中为何使用volatile关键字进行修饰? 此处使用的是volatile特性中的禁止指令重排,保证线程安全。
  • 为什么还会存在线程安全问题? 原因在于某一个线程执行到第一次检测,读取到的singleton不为null时,singleton的引用对象可能还没有完成初始化。 singleton = new Singleton();可以分为一下三步完成(通过字节码得到的伪代码) 字节码可以通过cmd进入Java文件所在目录,运行命令javac ./xxx.javajavap -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. 为什么两次判断为空?
  • 第一次判断为空:为了提高代码的执行效率,由于单例模式中只需要创建一次实例,所以当创建完成之后,再次调用时无需进入相同的代码块。
  • 第二次判断为空:防止二次创建实例。 可能会存在以下情形:<1>t1线程执行代码,正常通过两次校验,正准备创建Singleton实例 <2>系统资源被t2线程抢夺,也正常通过两次校验(因为此时t1线程还未创建实例),并创建了Singleton实例 <3>此时t1重新获取系统资源后,通过第二个判空发现已经存在实例则不会再重新创建,而是直接返回
  1. 使用synchronized
  • 为了保证某一时刻只有一个线程进行操作,保证了线程安全。

技术有限,定有不合理之处,欢迎指正并讨论。