目录
[修复方案:用 volatile 禁止重排序](#修复方案:用 volatile 禁止重排序)
面试题:
下面这段双重检查锁定(Double-Checked Locking)实现的单例模式,有什么问题?如何修复?
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
大多数面试者的回答(错误或不完整):
"没问题啊,加了
synchronized,是线程安全的。"
或者:
"应该加
volatile,防止指令重排序。"
------但为什么需要 volatile?不加到底会发生什么?很多人说不清楚。
问题本质:指令重排序导致"部分初始化"对象被泄露
instance = new Singleton(); 这行代码在 JVM 中并非原子操作,实际分为三步:
- 分配内存空间;
- 调用构造函数,初始化对象;
- 将
instance引用指向分配的内存地址。
但步骤 2 和 3 可能被 JIT 编译器重排序为:1 → 3 → 2!
后果:其他线程可能拿到一个"未完全初始化"的对象
- 线程 A 执行到步骤 3(
instance已非 null),但尚未完成构造(步骤 2); - 线程 B 进入
getInstance(),发现instance != null,直接返回; - 线程 B 使用这个对象时,可能访问到未初始化的字段(默认值),导致 NPE 或逻辑错误!
这不是理论风险!在高并发 + 特定 CPU 架构(如 ARM)下极易复现。
修复方案:用 volatile 禁止重排序
private static volatile Singleton instance; // 关键!
volatile的 happens-before 语义保证:- 写操作(步骤 3)前的所有操作(包括构造函数)对读线程可见;
- 禁止步骤 2 和 3 的重排序。
Bonus:更优解(推荐)
-
静态内部类(利用类加载机制) :
public class Singleton { private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }- 线程安全、懒加载、无锁、性能高,且无需处理重排序问题。