一句话说透Java里面的双重检查单例,为啥要加volatile

227 阅读2分钟

一句话回答
为了防止指令重排序导致其他线程拿到未初始化完成的对象!


一、问题根源:指令重排序

在Java中,对象的实例化操作 instance = new Singleton() 并不是原子的,它实际分为三步:

  1. 分配内存空间(为对象分配内存)。
  2. 初始化对象(执行构造函数,填充字段)。
  3. 将引用指向内存地址(将 instance 变量指向分配好的内存)。

关键问题

  • 编译器或CPU可能对步骤2和3进行重排序,导致其他线程看到一个未初始化完成的“半成品”对象。

二、错误场景模拟

假设没有 volatile,且发生了指令重排(步骤3在步骤2之前执行):

  1. 线程A 开始创建对象:

    • 执行步骤1(分配内存) → 步骤3(引用指向内存) → 此时 instance 已非 null,但对象未初始化!
  2. 线程B 调用 getInstance()

    • 第一次检查 instance != null → 直接返回未初始化的对象!
    • 使用这个对象时可能崩溃(如空指针异常)。

三、volatile 的作用

  1. 禁止指令重排序

    • 通过内存屏障(Memory Barrier)确保 步骤3(赋值)必须在步骤2(初始化)之后执行
  2. 保证可见性

    • 线程A修改 instance 后,其他线程能立即看到最新值。

四、正确代码示例

public class Singleton {
    // 必须加 volatile!
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查(无锁)
            synchronized (Singleton.class) {        // 加锁
                if (instance == null) {             // 第二次检查(加锁后)
                    instance = new Singleton();     // 安全创建
                }
            }
        }
        return instance;
    }
}

五、总结

  • 不加 volatile 的 DCL:可能因指令重排返回未初始化的对象,导致程序异常。

  • 加 volatile

    • 确保初始化完成后再赋值引用。
    • 保证多线程下变量的可见性。

口诀
「双重检查单例妙,不加 volatile 会乱套
指令重排半成品,volatile 屏障来护道!」