理解 JVM 的 8 个原子操作与 volatile
的语义
在并发编程中,volatile
常常被提及。但要真正理解它的作用,需要先回到 Java 内存模型(JMM) 的基础:线程与主内存之间的交互。JMM 定义了 8 个原子操作 来描述这些交互过程,而 volatile
的语义正是基于它们实现的。
JVM 的 8 个原子操作
JMM 把 主内存 、工作内存 和 执行引擎 的数据交互拆分成了 8 个最小操作,这些操作是并发语义的基石:
-
lock(锁定)
将主内存中的变量标记为某个线程独占。
-
unlock(解锁)
解除独占标记,使变量可被其他线程访问。
-
read(读取)
从主内存中读取变量值,传输到线程的通道。
-
load(载入)
将
read
到的值写入线程工作内存的变量副本。 -
use(使用)
将工作内存中的值传给执行引擎使用。
-
assign(赋值)
将执行引擎计算后的值赋给工作内存中的变量副本。
-
store(存储)
将工作内存的值传输到通道。
-
write(写入)
将
store
传来的值写入主内存。
四组配对关系
这 8 个操作其实是 四组成对动作:
- lock / unlock:保证变量在某一时刻只有一个线程独占。
- read / load:把主内存的值带入工作内存。
- store / write:把工作内存的值写回主内存。
- use / assign:工作内存与执行引擎之间的数据交互。
示意图:
主内存 <──read/load──> 工作内存 <──use/assign──> 执行引擎
^ │
└─────────────store/write───────────┘
其中 use/assign 仅在 线程内部 发生,对跨线程的可见性影响不大,因此开发者平时不太关心它。
volatile
的语义
volatile
并不会改变 8 个原子操作的存在方式,而是通过 内存屏障 来影响其中部分操作的执行顺序和可见性:
-
保证可见性
- 写入
volatile
变量:强制执行 store → write,立即刷新主内存。 - 读取
volatile
变量:强制执行 read → load,直接从主内存取值。
- 写入
-
保证有序性
- 在
volatile
写操作前后,JVM 插入 StoreStore、StoreLoad 屏障,防止指令重排。 - 在
volatile
读操作后,插入 LoadLoad、LoadStore 屏障,确保读到的值对后续操作可见。
- 在
-
不保证原子性
volatile
只能保证单次读/写的原子性。- 对于复合操作(如
count++
),依然会分解成多步(read → load → use → assign → store → write),在多线程下可能出现 丢失更新。
为什么 use / assign 看似多余?
有人会觉得 use/assign 没有必要,因为我们写代码时从未直接操作过它。实际上:
- read/load、store/write 关注的是 线程与主内存之间 的数据交换。
- use/assign 关注的是 线程工作内存与执行引擎之间 的交互。
这部分虽然我们平时感知不到,但在 JMM 的规范 层面,它是必须被定义的,否则无法保证指令级别的精确定义。
总结
- JVM 的 8 个原子操作分别是:
lock / unlock / read / load / use / assign / store / write
。 - 它们可以配对为四组:
- lock/unlock
- read/load
- store/write
- use/assign
volatile
主要约束的是 read+load、store+write ,配合内存屏障保证 可见性 + 有序性。volatile
不保证复合操作的原子性 ,如果需要保证,需要 CAS 或 锁。
✍️ 记住一句话:
volatile
让读写操作直达主内存,并通过内存屏障保证顺序,但不会把复合操作变成原子操作。