双重检查锁不用volatile会出错,因JVM可能重排序对象构造步骤,导致其他线程看到未初始化完成的实例,引发NullPointerException或字段为默认值;必须用volatile禁止重排序,且JDK 5+才真正支持。

为什么不用 volatile 的双重检查锁会出错
因为 JVM 可能对对象构造过程重排序,导致其他线程看到一个「已分配内存但未初始化完成」的实例。这不是多线程读写冲突,而是编译器和 CPU 对 new Singleton() 拆解后三步(分配内存 → 初始化 → 赋值给静态变量)的执行顺序不保证。
典型现象:某个线程调用 getInstance() 返回非 null,但后续访问其字段时抛 NullPointerException,或字段值为默认值(如 0、null)。
-
instance字段必须声明为volatile,否则无法禁止「初始化 → 赋值」之间的重排序 - JDK 5+ 才真正支持
volatile的禁止重排序语义;JDK 4 及之前即使写了也无效 - 仅加
synchronized块不够——它只保同步块内有序,不约束同步块外的赋值可见性
双重检查锁的标准写法长什么样
核心是:volatile 修饰静态实例 + 两次 null 检查 + 同步块内再判空。漏掉任意一环都可能失效。
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(); // 这一行必须在 synchronized 内
}
}
}
return instance;
}
}
-
instance必须是private static volatile,不能是final(构造在运行时) - 两个
if (instance == null)缺一不可:第一个避免无谓同步,第二个防止多线程同时通过第一层后重复创建 -
new Singleton()一定在synchronized块内部,且不能拆成两行(比如先声明变量再赋值)
什么情况下不该用双重检查锁
不是所有单例都适合——它解决的是「延迟初始化 + 高并发 + 无状态/轻构造」场景。一旦条件不满足,反而引入复杂性和风险。
立即学习“Java免费学习笔记(深入)”;
- 构造函数耗时长(如加载配置、连数据库):首次调用会阻塞所有后续线程,不如提前初始化(
static块)或用 Holder 模式 - 需要传参或依赖注入:双重检查锁无法支持构造参数,应改用 Spring 管理或显式工厂
- 类被反射或序列化破坏单例:
volatile不解决这个问题,得配合readResolve()或枚举 - Android 环境下低版本(API volatile 语义支持不稳定,建议直接用静态内部类
Holder 模式比双重检查锁更简单可靠?
是的。它利用 Java 类加载机制的天然线程安全和懒加载,完全规避了重排序和同步问题,代码更短、可读性更高。
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- 第一次调用
getInstance()时才触发Holder类加载,而类初始化由 JVM 保证线程安全 - 没有
synchronized、没有volatile、没有双重判断,也就没有记错、漏写、顺序错的风险 - 唯一代价是多一个嵌套类,但现代 JVM 早已对此优化,无实际性能损失
真正容易被忽略的是:很多人知道双重检查锁要加 volatile,却不知道 Holder 模式根本不需要任何额外关键字——它靠的是语言规范本身,而不是程序员的手动防护。


















