volatile关键字
volatile 是 Java 中最轻量的可见性工具。它能保证变量修改对其他线程可见,并禁止特定指令重排序,但不能保证复合操作的原子性。
# 1. volatile 解决什么
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
doWork();
}
}
如果没有 volatile,工作线程可能长期看不到 running=false。
# 2. 可见性
普通变量:
线程 A 修改 shared = 1
│
▼
修改可能暂存在工作内存/缓存
线程 B 读取 shared
│
▼
可能仍看到旧值
volatile 变量:
线程 A 写 volatile
│
▼
对其他线程可见
线程 B 读 volatile
│
▼
看到最新写入结果
# 3. 禁止重排序
volatile 写和读会建立内存屏障,限制指令重排序。
典型场景:状态标记。
data = loadData();
ready = true; // volatile
另一个线程:
if (ready) {
use(data);
}
ready 是 volatile 时,写 ready=true 之前对 data 的写入,对读到 ready=true 的线程可见。
# 4. volatile 不能保证原子性
private volatile int count;
public void increment() {
count++;
}
count++ 包含三步:
读 count
加 1
写 count
volatile 只能保证每次读写可见,不保证三步合成一个不可分割操作。
多线程计数应使用:
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
或加锁。
# 5. volatile happens-before
规则:
对 volatile 变量的写
happens-before
后续对同一个 volatile 变量的读
示意:
// 线程 A
config = newConfig;
initialized = true; // volatile 写
// 线程 B
if (initialized) { // volatile 读
use(config);
}
线程 B 读到 initialized=true 后,也能看到线程 A 在此之前写入的 config。
# 6. 适用场景
适合:
- 状态标记。
- 开关控制。
- 单写多读配置引用。
- 双重检查锁中的实例引用。
- 发布不可变对象引用。
不适合:
- 复合操作,例如
i++。 - 多字段一致性。
- 需要互斥的临界区。
- 条件等待和通知。
# 7. 双重检查锁
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 防止对象引用赋值与构造初始化之间发生对外可见的重排序。
对象创建概念步骤:
分配内存
初始化对象
引用指向内存
如果引用先对外可见,其他线程可能拿到未初始化完成的对象。
# 8. volatile 与 synchronized
| 对比项 | volatile | synchronized |
|---|---|---|
| 可见性 | 保证 | 保证 |
| 有序性 | 保证特定顺序 | 保证临界区顺序 |
| 原子性 | 单次读写 | 临界区整体 |
| 阻塞 | 不阻塞 | 可能阻塞 |
| 适用 | 状态标记、发布引用 | 复合操作、多字段一致性 |
volatile 是轻量工具,不是锁的替代品。
# 专家实践与边界
volatile 的关键是“轻量可见性”,不是“轻量锁”。它适合状态标记、配置引用发布、单写多读开关,不适合保护多个变量之间的不变量。
线程 A 写 volatile
│ 刷新写入并建立 happens-before
▼
线程 B 读 volatile
│ 看到最新值
▼
后续读操作也能看到写入前的相关状态
| 场景 | 是否适合 volatile | 原因 |
|---|---|---|
| 停止标记 | 适合 | 单变量可见性 |
| 配置对象整体替换 | 适合 | 引用不可变或安全发布 |
count++ | 不适合 | 不是原子操作 |
| 多字段一致性 | 不适合 | 需要锁保护不变量 |
| 双重检查单例 | 可用 | 需要禁止重排序 |
排查 volatile 相关 bug 时,要看代码是否把“看到最新值”误解成“复合操作安全”。如果一个判断后面跟着修改共享状态,通常已经超出 volatile 的能力范围。
# Tips 快问快答
Q:volatile 保证什么? A:保证可见性,并禁止相关指令重排序。
Q:volatile 能保证 i++ 原子吗?
A:不能,i++ 是复合操作。
Q:volatile 会加锁吗? A:不会,它不会让线程阻塞。
Q:什么时候适合 volatile? A:状态开关、单写多读标记、发布不可变对象引用。
Q:volatile 和 synchronized 最大区别是什么? A:volatile 不提供互斥,synchronized 提供互斥和临界区原子性。
Q:双重检查锁为什么要 volatile? A:防止实例引用在对象初始化完成前对其他线程可见。
Q:volatile 读到 true 后能看到之前写入的数据吗? A:能,volatile 写 happens-before 后续同变量 volatile 读。
Q:多个变量一致性可以靠 volatile 吗? A:通常不能,需要锁或不可变对象整体发布。