跳到主要内容

volatile、屏障与可见性

volatile 适合表示一个可以独立读写的共享变量,或者充当状态发布信号。它为读写提供可见性和顺序保证,但不为多步操作或多个字段的不变量提供互斥。

1. volatile 写与后续读建立顺序

private volatile boolean closed;

public void close() {
closed = true;
}

public void runLoop() {
while (!closed) {
processOne();
}
}

closed 的 volatile 写 happens-before 另一个线程随后观察到该写的 volatile 读。JVM 不能把相关访问重排到破坏这条关系的位置,读线程也必须按规范观察值。

它适合状态标记、不可变配置引用和满足单写者等条件的状态机。

2. volatile 不保护复合更新

private volatile int count;

public void increment() {
count++;
}

count++ 是读取、加一、写回。每次读和写具有 volatile 语义,整个组合仍可被其他线程穿插,因此会丢失更新。需要精确计数时使用 AtomicInteger.incrementAndGet() 或锁。

同样,下面的检查后执行也不安全:

if (!initialized) {
initialize();
initialized = true;
}

多个线程可能同时通过检查。volatile 不能代替互斥。

3. 发布的是一条有方向的边

private Data data;
private volatile boolean ready;

void publish(Data next) {
data = next;
ready = true;
}

消费线程先读到 ready == true,再读取 data,可以利用传递的 happens-before 看到发布前的普通写。若先读 data、后读 ready,或者读的是另一个标志,就没有这条同样的证明。

数组引用声明为 volatile 也只约束引用本身,普通数组元素不会因此全部获得 volatile 语义。需要原子数组时使用 AtomicIntegerArray 等类型。

4. 内存屏障是实现手段

JIT 会根据目标处理器把 volatile 与 VarHandle 的访问模式映射为相应的编译器约束、机器指令和屏障。不同架构的成本与具体屏障组合并不相同。

工程代码应围绕 acquire/release 和 happens-before 表达需求,不要把某张 x86 屏障表当成 Java 规范。需要更细粒度语义的底层组件可以使用 VarHandle 的 plain、opaque、acquire、release 和 volatile 模式,但普通业务代码很少需要主动降低保证。

5. 典型使用边界

需求volatile 是否合适原因
关闭标记通常合适单个状态读写
发布不可变配置合适引用切换和初始化写建立顺序
多线程精确累加不合适需要原子读改写
余额检查并扣减不合适多步不变量需要互斥或 CAS 协议
两个字段必须同时一致通常不合适独立 volatile 写会暴露中间组合

6. 常见问题

6.1 volatile long 的意义只是防止撕裂吗

不只是。现代 Java 对单次 volatile 读写还规定了内存顺序和可见性语义。将它理解成“64 位原子写”会漏掉最重要的 happens-before 保证。

6.2 volatile 变量越多,线程安全性越高吗

不会。多个变量分别可见,仍不能保证组合状态一致。应先写出不变量,再确定同步边界。

7. 面试题

7.1 为什么 volatile 不能保证 i++ 的线程安全

出现公司:腾讯、阿里巴巴、蚂蚁集团

考察重点

  • 单次 volatile 访问与复合操作的区别。
  • 丢失更新的交错过程。
  • 合适的原子类或锁方案。

相关内容:第 1 节“volatile 写与后续读建立顺序”和第 2 节“volatile 不保护复合更新”。

参考回答

volatile 读写各自有可见性和顺序保证,但 i++ 包含读、计算、写三个步骤。两个线程可以读到同一个旧值,再分别写入相同的新值,所以一次更新丢失。

只维护单个计数时可以用 AtomicInteger.incrementAndGet();如果更新还依赖其他字段或前置条件,则要把完整不变量放进锁或设计正确的 CAS 循环。