必须用volatile修饰静态实例变量,否则多线程下可能拿到未初始化完成的对象;因为JVM创建对象的三步(分配内存、调用构造函数、赋值引用)可能重排序,导致其他线程看到半初始化状态。

必须用 volatile 修饰静态实例变量,否则多线程下可能拿到未初始化完成的对象。
为什么 instance = new Singleton() 会出问题
JVM 对对象创建的三步操作(分配内存、调用构造函数、赋值引用)可能重排序。没有 volatile 时,线程 A 可能先执行“分配内存 + 赋值引用”,再执行“构造函数”,此时线程 B 看到 instance != null 就直接返回,但实际 Singleton 的字段还是默认值(如 null、0),后续调用会 NPE 或逻辑错误。
加 volatile 后,写操作会插入内存屏障,禁止构造函数调用被重排到赋值之后,同时保证其他线程读到该变量时能看到完整的初始化结果。
synchronized 块里为什么还要二次判空
第一次判空在同步外,是为了避免每次调用 getInstance() 都进锁——这是性能关键点;第二次判空在锁内,是因为多个线程可能同时卡在第一次检查后、抢到锁前,若不二次检查,就会重复初始化。
- 不加第二次检查 → 多个线程都执行
new Singleton()→ 多个实例 - 不加
volatile→ 即使只一个线程执行new,其他线程也可能看到半初始化状态 - 锁对象必须是类本身(如
Singleton.class),不能用this,因为静态方法里没有实例
典型实现中容易漏掉的细节
常见错误不是语法错,而是语义疏忽:
- 构造函数没设为
private→ 外部可new破坏单例 -
instance没加volatile→ 并发下不可靠(JDK 5+ 才真正支持volatile的内存语义) - 把
synchronized加在方法上(public static synchronized Singleton getInstance())→ 完全失去 DCL 的性能优势 - 初始化逻辑里抛异常但没处理 → 第二次调用仍会尝试创建,可能反复失败
示例代码核心片段:
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 构造函数必须无参且私有
}
}
}
return instance;
}
真正难的是理解「可见性」和「重排序」不是理论问题,而是在高并发压测时才会暴露的偶发崩溃。哪怕测试跑一万次不失败,也不能说明它安全——只要没 volatile,就永远存在风险。


















