一句话回答:
为了防止指令重排序导致其他线程拿到未初始化完成的对象!
一、问题根源:指令重排序
在Java中,对象的实例化操作 instance = new Singleton() 并不是原子的,它实际分为三步:
- 分配内存空间(为对象分配内存)。
- 初始化对象(执行构造函数,填充字段)。
- 将引用指向内存地址(将
instance变量指向分配好的内存)。
关键问题:
- 编译器或CPU可能对步骤2和3进行重排序,导致其他线程看到一个未初始化完成的“半成品”对象。
二、错误场景模拟
假设没有 volatile,且发生了指令重排(步骤3在步骤2之前执行):
-
线程A 开始创建对象:
- 执行步骤1(分配内存) → 步骤3(引用指向内存) → 此时
instance已非null,但对象未初始化!
- 执行步骤1(分配内存) → 步骤3(引用指向内存) → 此时
-
线程B 调用
getInstance():- 第一次检查
instance != null→ 直接返回未初始化的对象! - 使用这个对象时可能崩溃(如空指针异常)。
- 第一次检查
三、volatile 的作用
-
禁止指令重排序:
- 通过内存屏障(Memory Barrier)确保 步骤3(赋值)必须在步骤2(初始化)之后执行。
-
保证可见性:
- 线程A修改
instance后,其他线程能立即看到最新值。
- 线程A修改
四、正确代码示例
public class Singleton {
// 必须加 volatile!
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查(加锁后)
instance = new Singleton(); // 安全创建
}
}
}
return instance;
}
}
五、总结
-
不加
volatile的 DCL:可能因指令重排返回未初始化的对象,导致程序异常。 -
加
volatile:- 确保初始化完成后再赋值引用。
- 保证多线程下变量的可见性。
口诀:
「双重检查单例妙,不加 volatile 会乱套
指令重排半成品,volatile 屏障来护道!」