JMM本身不防范漏洞,而是通过规则和同步机制提供并发控制手段;DCL单例出问题源于未遵循JMM规则,特别是对象构造三步操作(分配、初始化、赋值)可能被重排序为1→3→2,导致线程获取到未初始化对象;volatile通过写屏障禁止1、2重排到3之后,确保instance赋值前对象已构造完成;synchronized无法约束new内部指令顺序,故必须配合volatile使用;正确DCL需满足:instance用volatile修饰、两次null检查、构造函数不暴露未完成状态。

Java 内存模型(JMM)本身不主动“防范”漏洞,而是通过明确的规则和同步机制,为程序员提供控制并发行为的手段。DCL(双重检查锁定)单例模式出问题,根本原因不是 JMM 有缺陷,而是代码没遵循 JMM 规则——特别是忽略了对象构造的三步操作在多线程下可能被重排序。JMM 配合 volatile 才能堵住这个漏洞。
对象初始化的三步操作是重排序根源
new Singleton() 并非原子操作,实际包含:
- 分配内存空间(memory = allocate())
- 在该内存中初始化对象(ctor(memory))
- 将 instance 引用指向这块内存(instance = memory)
正常顺序是 1→2→3,但 JVM 或 CPU 可能优化为 1→3→2。此时若线程 A 执行完第 3 步就挂起,线程 B 进入 if (instance != null) 判断,会直接返回一个“已分配内存但未初始化”的引用——字段仍是默认值(如 null、0、false),后续调用极易抛 NPE 或产生不可预知逻辑错误。
volatile 如何用写屏障切断危险重排序
把 static instance 声明为 volatile,不是为了“让别的线程看到新值”(可见性只是副产品),核心作用是插入写屏障(Store Barrier):
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
- 禁止编译器/JVM 把步骤 1(分配)和步骤 2(初始化)重排到步骤 3(赋值)之后
- 强制保证:instance 赋值前,内存分配和对象构造必须全部完成
- 等效于给 new 操作加了一道“完成门”,确保其他线程拿到的 instance 必然指向一个构造完毕的对象
为什么 synchronized 本身不够
DCL 中的 synchronized 锁住的是类对象,它能保证临界区串行执行,也能建立锁释放-获取的 happens-before 关系,但它无法约束锁内 new 操作内部的指令顺序。也就是说,即使两个线程都排队进 synchronized 块,只要 instance 字段没加 volatile,线程 A 在块内执行 instance = new Singleton() 时,仍可能发生 1→3→2 重排序,线程 B 在块外读到非 null 的 instance 就可能踩坑。
正确写法与关键细节
标准 DCL 单例必须同时满足三项:
- instance 字段用 volatile 修饰(缺一不可)
- getInstance() 方法内做两次 null 检查(外层避免无谓加锁,内层防止重复初始化)
- 构造函数无参数或不含对外暴露未完成状态的逻辑(比如不要在构造中启动线程或发布 this)
示例:
private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // volatile 确保这行不会被重排序破坏 } } } return instance; }

















