在JMM java内存模型中,当读一个 volatile 变量时, JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值
线程在【写入】共享变量时,不会把值缓存在寄存器或其他地方(就是刚刚说的所谓的「工作内存」),而是会把值刷新回主内存 。
为啥读取的时候,先清空本地内存,再去主内存获取最新值?
为啥不直接读取主内存呢?自己的工作内存有什么用呢?
简单直接的回答是:
- 线程不可能"直接"读取主内存(物理 RAM),因为速度太慢了(相差几百倍)。
- 清空本地内存"是一个逻辑动作,不是物理擦除 。它的真实含义是:强制让 CPU 缓存行失效(Invalidate),迫使 CPU 在下一次读取时去别处(其他核心缓存或主存)拉取最新数据,而不是使用自己缓存里的旧副本。
- 工作内存(即 CPU 缓存 + 寄存器):它是为了性能而存在的。如果没有它,现代计算机的速度会倒退 30 年。
一、为什么不能"直接读取主内存"?
物理现实:
- CPU 寄存器/一级缓存 (L1):速度极快,约 1 纳秒 (1ns)。
- 主内存 (RAM):速度相对很慢,约 100 纳秒 (100ns)。
- 差距:访问主内存的延迟是访问缓存的 100 倍 以上。
如果每次读取变量(哪怕是普通局部变量)都直接去摸主内存,CPU 绝大部分时间都在"发呆"等待数据返回,整个系统的吞吐量会崩塌。
结论: 无论是否 volatile,CPU 永远 是先将数据从主内存加载到 CPU 缓存(工作内存),然后由寄存器进行计算。不存在"跳过缓存直接读主存"这种操作模式(除了极少数特殊的非临时内存指令,但 Java 普通变量不走这个路径)。
二、所谓的"清空本地内存"到底发生了什么?
你在 JMM 规范中看到的"清空工作内存",绝对不是指把 CPU 缓存里的二进制位全部抹成 0。
1. 这是一个"逻辑标记",而非"物理擦除"
当线程读取 volatile 变量时,JMM 要求的"清空"在硬件层面是通过 内存屏障 (Load Barrier) 触发的 缓存一致性检查。
- 场景:线程 A 要读 volatile int x。
- 动作:
- CPU 执行读指令前,遇到 LoadLoad 或 LoadStore 屏障。
- 屏障指令告诉 CPU:"请检查你缓存里关于 x 的那一行数据是不是最新的?"
- MESI 协议介入:
- 如果其他核心修改过 x,当前核心缓存中 x 所在行的状态会被标记为 Invalid (I)(这就是所谓的"逻辑清空")。
- 如果状态是 Invalid,CPU 被迫 发起总线请求,从主内存或其他核心的缓存中拉取最新数据,更新到自己的缓存中,并将状态改为 Shared (S) 或 Exclusive (E)。
- 最终读取:CPU 实际上是从刚刚更新过的本地缓存中读取了数据。
所以,"清空"的真实含义是:废弃旧的缓存副本,强制重新加载。
2. 为什么要多此一举?
如果不"清空"(不检查有效性):
- 线程 A 的缓存里可能还留着昨天线程 B 修改前的旧值(状态可能是 S 或 E,但在没有收到失效广播前,它以为自己是对的)。
- 直接读,就读到了脏数据。
- 通过"逻辑清空"(触发失效检查),保证了可见性。
三、工作内存(缓存)到底有什么用?
既然 volatile 读写都要折腾主内存(或跨核心同步),那工作内存是不是没用了?大错特错!
工作内存(CPU 缓存)在 volatile 场景下依然起着至关重要的作用:
1. 数据的"中转站"和"加速器"
即使是 volatile 写操作:
- 步骤:寄存器 -> 本地缓存 (触发 MESI 广播) -> (异步) -> 主内存。
- 作用:数据必须先落入本地缓存才能触发 MESI 协议。如果没有本地缓存,CPU 无法高效地管理缓存一致性状态(M/E/S/I)。
即使是 volatile 读操作:
-
步骤:(检查失效) -> 从主存/其他核拉取 -> 写入本地缓存 -> 寄存器读取。
-
作用:数据最终还是落在了本地缓存里。如果紧接着第二次读取同一个 volatile 变量(且期间无其他核修改),由于此时缓存状态是有效的(S 或 E),第二次读取可以直接命中缓存,无需再次访问主内存!
-
注意:volatile 保证的是"每次读取都能见到最新值",而不是"每次读取都必须物理访问主内存"。只要缓存是最新的,读缓存就是合法的。
2. 对非 volatile 变量的极致优化
一个线程中绝大多数变量是非 volatile 的(如局部变量、私有字段)。
- 对于这些变量,线程可以肆无忌惮地将它们缓存在工作内存(寄存器/L1 缓存)中,甚至重排序,完全不用管主内存。
- 这带来了巨大的性能提升。如果没有工作内存,所有变量访问都变慢 100 倍,程序将无法运行。
3. 减少总线风暴
如果没有工作内存,每个核心每次读写都直接轰击主内存控制器:
- 主内存带宽瞬间被占满。
- 多核之间无法利用"缓存共享"(比如多个核读同一个常量,有了缓存,只有第一个核去主存拿,后面大家都从缓存拿;没缓存,大家都去挤主内存)。
四、图解流程对比
为了彻底理清,我们对比一下 普通变量 和 volatile 变量 的读取流程:
场景:线程 A 读取变量 x
| 步骤 | 普通变量 (x) |
Volatile 变量 (volatile x) |
硬件行为解析 |
|---|---|---|---|
| 1. 准备 | 直接使用寄存器/缓存中的值。 | 插入内存屏障。 | 普通变量假设本地副本永远有效(除非发生上下文切换等特定情况)。 |
| 2. 检查 | 不检查一致性。 | 检查缓存行状态。 | volatile 强制触发 MESI 嗅探。如果状态是 Invalid,说明别人改过了。 |
| 3. 获取 | 直接从本地缓存/寄存器取值。 | 若状态无效,发起总线请求,从主存或其他核拉取新值,更新本地缓存。 | 这就是所谓的"清空旧值,从主内存获取"。实际上是更新了本地缓存。 |
| 4. 读取 | 读取本地副本。 | 从已更新的本地缓存取值。 | 重点:最终还是从本地缓存读的!没有谁能直接从 RAM 读到寄存器。 |
| 5. 后续 | 下次读继续用本地副本(可能是脏数据)。 | 下次读若缓存未失效,可直接用本地副本(此时是新的)。 | volatile 只保证这一次读是新的,不禁止利用缓存加速后续的读。 |
