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