JMM java内存模型分析

在JMM java内存模型中,当读一个 volatile 变量时, JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值
线程在【写入】共享变量时,不会把值缓存在寄存器或其他地方(就是刚刚说的所谓的「工作内存」),而是会把值刷新回主内存


为啥读取的时候,先清空本地内存,再去主内存获取最新值?

为啥不直接读取主内存呢?自己的工作内存有什么用呢?

简单直接的回答是:

  1. 线程不可能"直接"读取主内存(物理 RAM),因为速度太慢了(相差几百倍)。
  2. 清空本地内存"是一个逻辑动作,不是物理擦除 。它的真实含义是:强制让 CPU 缓存行失效(Invalidate),迫使 CPU 在下一次读取时去别处(其他核心缓存或主存)拉取最新数据,而不是使用自己缓存里的旧副本。
  3. 工作内存(即 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。
  • 动作:
    1. CPU 执行读指令前,遇到 LoadLoad 或 LoadStore 屏障。
    2. 屏障指令告诉 CPU:"请检查你缓存里关于 x 的那一行数据是不是最新的?"
    3. MESI 协议介入:
      • 如果其他核心修改过 x,当前核心缓存中 x 所在行的状态会被标记为 Invalid (I)(这就是所谓的"逻辑清空")。
      • 如果状态是 Invalid,CPU 被迫 发起总线请求,从主内存或其他核心的缓存中拉取最新数据,更新到自己的缓存中,并将状态改为 Shared (S) 或 Exclusive (E)。
    4. 最终读取: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 只保证这一次读是新的,不禁止利用缓存加速后续的读。
相关推荐
不会写DN2 小时前
Js常用数组处理
开发语言·javascript·ecmascript
回到原点的码农2 小时前
Spring Boot 热部署
java·spring boot·后端
还是大剑师兰特2 小时前
数组中有两个数据,将其变成字符串
开发语言·javascript·vue.js
ameyume2 小时前
设计模式之单例模式的线程安全
java
2301_776508722 小时前
C++中的职责链模式实战
开发语言·c++·算法
Java烘焙师2 小时前
AI编程实战:从零到一搭建全栈项目
java·架构·树莓派·ai实战
sqyno1sky2 小时前
C++中的空对象模式
开发语言·c++·算法
星轨初途2 小时前
C++ 类和对象(下):初始化列表、static 成员与编译器优化深度剖析
android·开发语言·c++·经验分享·笔记
量子炒饭大师2 小时前
【C++ 入门】Cyber动态义体——【vector容器】vector底层原理是什么?该怎么使用他?一文带你搞定所有问题!!!
开发语言·c++·vector·dubbo