必须用 volatile 修饰实例字段,否则因指令重排序可能导致其他线程看到未初始化完成的对象;volatile 禁止重排序并保证可见性,确保对象初始化完成后再赋值引用。

Java 中双重检查锁(DCL)单例模式必须用 volatile 修饰实例字段,否则可能因指令重排序导致其他线程看到未初始化完成的对象。
为什么需要 volatile
在 DCL 中,new 操作不是原子的,它分为三步:
- 分配内存空间
- 在内存中初始化对象(调用构造方法)
- 将引用赋值给静态变量(instance = new Singleton())
JVM 和 CPU 可能对 2 和 3 步进行重排序。若线程 A 执行到第 3 步(引用已赋值),但第 2 步还没完成,此时线程 B 进入 if (instance == null) 判断,发现 instance 不为 null,直接返回该引用——而这个对象尚未初始化完毕,调用其方法会出错(如 NullPointerException 或状态不一致)。
volatile 禁止指令重排序,并保证可见性:一旦 instance 被写入,其他线程立即能看到最新值,且写入前的所有操作(包括对象初始化)都已完成。
立即学习“Java免费学习笔记(深入)”;
标准写法(推荐)
以下是线程安全、符合 Java 内存模型的 DCL 单例实现:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // volatile 保证此处的写入安全
}
}
}
return instance;
}
}
常见错误写法
以下写法看似简洁,但存在严重隐患:
- 去掉
volatile:重排序风险依然存在,JDK 5 之前尤其危险 - 把
synchronized加在方法上(public static synchronized Singleton getInstance()):性能差,每次调用都加锁,失去 DCL 的意义 - 在构造器中启动线程或发布 this 引用:即使加了 volatile,也可能泄露未初始化完成的对象
替代方案对比
如果追求更简单、更安全的实现,可考虑:
- 静态内部类:利用类加载机制保证线程安全,无同步开销,推荐用于大多数场景
- 枚举单例:天然防止反射和反序列化攻击,最简洁安全
- 使用 Spring 管理单例 Bean:在应用上下文中交由框架托管,无需手动实现
volatile + DCL 是底层可控、性能较好但需谨慎编码的方式;日常开发中,优先选静态内部类或枚举。


















