深入理解单例模式:DCL 双重检查锁为什么必须加 volatile?

深入理解单例模式:DCL 双重检查锁为什么必须加 volatile?

_

在 Java 开发中,单例模式是我们最常用的设计模式之一。为了实现延迟加载并保证线程安全,双重检查锁(Double-Checked Locking,简称 DCL)是一种非常经典的写法。

然而,很多人在手写 DCL 单例时,往往只记得加上 synchronized 同步锁,却忽略了极其关键的 volatile 关键字。今天我们就来盘一盘,如果不加 volatile 会引发什么问题,以及它背后的底层原理。

1. 标准的 DCL 单例实现代码

先来看一个完全正确的 DCL 单例模式写法。这里有三个不可或缺的关键点:私有化构造方法静态获取方法,以及最重要的 volatile 修饰实例变量

public class Pig {
    // 核心关键点:必须使用 volatile 修饰,防止指令重排
    private static volatile Pig pig = null;

    // 关键点:私有化构造方法,防止外部直接 new 对象
    private Pig() {
    }

    // 关键点:提供全局访问的静态方法
    public static Pig getInstance() {
        // 第一重检查:如果对象已经实例化,直接返回,避免不必要的加锁开销
        if (pig == null) {
            // 加锁,保证同一时刻只有一个线程进入初始化逻辑
            synchronized (Pig.class) {
                // 第二重检查:防止多个线程同时通过了第一重检查,导致重复初始化
                if (pig == null) {
                    pig = new Pig();
                }
            }
        }
        return pig;
    }
}

2. 为什么不加 volatile 会导致“半初始化”?

很多同学会有疑问:既然已经有了 synchronized 保证了同步,为什么还会出问题?

问题的根源出在 pig = new Pig(); 这行代码上。在 Java 中,创建一个对象并不是一个原子操作。在 JVM 层面,这行简单的代码大致会被拆分为以下三个汇编指令步骤:

  1. 分配内存空间allocate):在堆内存中为 Pig 对象分配一块空间。

  2. 初始化对象init):调用 Pig 的构造方法,初始化成员变量。

  3. 设置引用指向assign):将 pig 这个引用变量指向刚分配出的内存地址。

危险的指令重排: 在单线程环境下,这三步无论按什么顺序执行,最终结果都是一致的。因此,为了优化执行效率,编译器和 CPU 可能会对这些指令进行重排(Instruction Reordering)。实际的执行顺序很可能会变成 1 -> 3 -> 2

高并发下的灾难现场: 假设发生了 1 -> 3 -> 2 的指令重排,现在有线程 A 和线程 B 同时调用 getInstance()

  • 线程 A 获取到锁,执行了步骤 1 和步骤 3。此时,pig 引用已经指向了具体的内存地址,它已经不再是 null,但是真正的对象初始化(步骤 2)还未执行

  • 线程 B 此时正好执行到第一重检查 if (pig == null),发现 pig 已经不为空,于是直接拿走这个还没初始化的“半成品”对象去使用。

  • 最终结果:线程 B 在访问该对象的成员变量或方法时,极大概率会获取到默认值,甚至抛出 NullPointerException 异常。

3. volatile 是如何破局的?

为了解决这个致命隐患,我们需要在声明实例变量时加上 volatile 关键字。

volatile 在这里的核心作用就是禁止指令重排。它的底层实现依赖于内存屏障(Memory Barrier)技术。

简单来说,给变量加上 volatile 后,就相当于给 JVM 下了一道死命令:在对该变量进行写操作时,必须确保其前面的代码(如内存分配、对象初始化)全部执行完毕。 JVM 会在汇编指令中插入特定的屏障指令(如 StoreStore 和 StoreLoad 屏障),强行打断 CPU 的重排优化,从而保证了 1 -> 2 -> 3 的严格执行顺序。

4. 总结

在编写 DCL 单例模式时,两把锁各司其职,缺一不可:

  • synchronized:保证了复合操作的原子性和线程间的可见性,解决并发冲突导致的重复实例化问题。

  • volatile:保证了指令执行的有序性,依靠内存屏障防止指令重排,彻底杜绝了高并发下拿到“半初始化”对象的系统隐患。

告别繁琐配置:基于 Auth0 的超轻量级 Java JWT 工具类实现 2026-05-30
JDK 9+ 观察者模式最佳实践:告别废弃 API,拥抱 PropertyChangeSupport 2026-06-06

评论区