volatile修饰的i++操作被拆分为volatile读、寄存器计算、volatile写三步,每步具备可见性与有序性,但整体不原子,无法防止竞态条件。

volatile 修饰的变量在进行自增操作(如 i++)时,底层指令依然会被拆分为“读-改-写”三步,但 volatile 本身不保证这三步的整体原子性。它只确保每一步的读和写操作具有可见性和有序性,而无法阻止中间被其他线程插入执行。
volatile 变量的 i++ 是怎么拆的?
以 volatile int i = 0; 为例,执行 i++ 时,JVM 会将其编译为近似如下字节码(简化示意):
getstatic i // 从主内存读取 i 的最新值(volatile 读) iconst_1 // 推入常量 1 iadd // 执行加法(i + 1),在操作数栈完成 putstatic i // 将结果写回主内存(volatile 写)
对应到底层 CPU 指令(x86 架构),大致是:
Load:
mov eax, [i_addr]
→ 强制从主内存(或通过 MESI 协议保证的最新缓存行)加载i值;
→ 触发LoadLoad+LoadStore内存屏障,禁止后续读/写重排序到该读之前。Compute:
add eax, 1
→ 在寄存器中完成计算;
→ 这一步不在 volatile 保护范围内,不涉及内存交互,也无同步语义。Store:
mov [i_addr], eax
→ 强制将新值写回主内存;
→ 触发StoreStore+StoreLoad内存屏障,确保前面的写已刷新、后面的读写不被提前。
⚠️ 关键点:第 2 步(计算)和第 1、3 步之间没有原子锁或 CAS 机制,因此:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 线程 A 读到
i=5→ 计算得6→ 尚未写回; - 线程 B 同时读到
i=5→ 计算得6→ 写回; - 线程 A 再写回
6;
→ 最终i只加了 1,而不是预期的 2。
这就是典型的竞态条件(race condition),volatile 完全无法防止。
为什么 volatile 不管用?核心原因有三:
- ✅ 单次读是原子的:
getstatic i一定拿到最新值; - ✅ 单次写是原子的:
putstatic i一定刷到主内存并使其他缓存失效; - ❌ 读+算+写不是原子的:三步之间无锁定,也不具备 CAS 的“比较并交换”能力;
- ❌ 没有互斥机制:
volatile不阻塞其他线程访问,也不参与锁竞争。
类比:
volatile像一个“透明公告栏”——每个人都能立刻看到别人刚贴的新纸条(可见性),也能立刻把自己的纸条贴上去(有序写入),但不能阻止两人同时抄下旧数字、各自加 1、再同时贴回去。
如果真要安全自增,该怎么做?
| 方式 | 底层关键机制 | 是否依赖 volatile |
|---|---|---|
synchronized 块 |
JVM 管理 monitor 锁,保证临界区互斥 | 不需要 volatile(锁已提供全部语义) |
AtomicInteger.incrementAndGet() |
CPU lock xadd 指令(原子读-改-写) |
内部字段用 volatile 保证可见性,但原子性靠硬件指令 |
Unsafe.compareAndSwapInt() |
直接调用 CAS 汇编指令(如 cmpxchg) |
配合 volatile 字段使用,但原子性由 CAS 保证 |
例如 AtomicInteger 的实现本质是:
// volatile 确保 value 始终从主内存读、写回主内存
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
→ getAndAddInt 底层循环调用 cmpxchg,失败则重试,直到 CAS 成功。
小结一句话
volatile i++ 的底层拆解是:volatile 读 → 寄存器计算 → volatile 写;
它把每一步都“暴露给所有线程”,但不把这三步打包成一个不可分割的动作——所以不是线程安全的自增。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










