Skip to main content

cpu 多级缓存 为什么需要jmm

JMM 的语境下,你可认为每个 CPU 核心(Core)都对应着一个线程的“工作内存”

但如果落实到真实的物理硬件(比如 9800X3D)上,“工作内存”并不是一个独立的物理芯片,而是由 寄存器、L1、L2 缓存 以及 L3 缓存的一部分 共同拼凑出来的。


1. 9800X3D 的硬件缓存真实分配

9800X3D 是一个 8核心(8 Cores) 的处理器。它的缓存分配采用的是 “两级私有,一级共享” 的金字塔结构:

缓存级别容量分配方式硬件特征 (速度与物理位置)对应 JMM 概念
寄存器 & L1 缓存极小 (每个核心几百KB)绝对私有速度最快,每个核心自己独占,别人无法访问。工作内存的“核心区”
L2 缓存8 MB (共 8 个核心)每核私有 1MB速度极快,核心 0 只能用自己的这 1MB,拿不到核心 1 的 L2。工作内存的“缓冲区”
L3 缓存96 MB (32+64MB)8个核心全部共享速度较快,但它是**切片(Sliced)**的。所有核心通过高速总线一起访问这块巨大的 96MB 缓存。主内存与工作内存的“交界处”

2. 为什么说“共享的 L3 缓存”也会变成“工作内存”?

“既然 L3 缓存是 8 个核心共享的,那它怎么能算某个核心的‘私有工作内存’呢?”*

因为 CPU 读写数据有一个“就近原则”(缓存行加载)。

  1. 当线程 A(在核心 0 上)修改了变量 count,这个数据会首先写入核心 0 的 L1 和 L2。
  2. 随后,它可能会被刷新到 L3 缓存中分配给核心 0 的那个“片区”
  3. 此时,虽然 L3 是物理共享的,但如果核心 0 没有把数据同步回真正的内存(RAM),或者没有触发缓存一致性协议去通知其他核心,那么对于核心 1 来说,核心 1 依然看不到这个修改。

所以在 JMM 的抽象视角里:只要是没有刷新到公共 RAM、且没有对其他核心公开的数据状态,都属于该线程的“工作内存”。


3. 硬件上,多核心之间是怎么通信的?(MESI协议)

既然硬件上有这么多层缓存,核心 0 怎么知道核心 1 修改了数据?硬件工程师在芯片里装了一套广播体操,叫做 MESI 缓存一致性协议

当核心 0 修改了自己 L2 缓存里的数据时,它会通过总线向全世界广播:

“我修改了 count 变量,你们谁的缓存里要是也有这个变量,立刻给我标记为‘失效(Invalid)’!”

核心 1 收到广播,默默把自己 L2 缓存里的 count 删掉。下次核心 1 再想用 count 时,发现缓存空了(Cache Miss),就必须穿透到 L3 甚至主内存(RAM)去读核心 0 刚刚写好的最新值。


4. 既然硬件有 MESI 协议,为什么还要 JMM 和 volatile?

这往往是最让人想不通的地方:“硬件都自动同步了,Java 为什么还要搞可见性问题?”

两个字:性能。

  1. CPU 会偷懒(Store Buffer):核心 0 修改数据后,如果每次都要等其他 7 个核心回应“我已失效”,CPU 就会陷入漫长的等待(卡顿)。为了跑得快,CPU 把修改往 Store Buffer(写缓冲区)一扔就去干别的事了,过一会才真正刷入缓存。在刷入之前,MESI 协议根本不知道这回事。
  2. 编译器会自作聪明:Java 的 JIT 编译器在编译代码时,如果发现一个变量在循环里没改变,它会直接把这个变量提升到寄存器里。寄存器是绝对私有的,MESI 协议根本管不到寄存器!
// 编译器优化导致的死循环
while (flag) {
// 如果 flag 没有加 volatile,JIT 编译器会把它读入寄存器
// 此时就算另一个核心通过 MESI 把主内存和 L3 里的 flag 改了
// 当前核心因为只看自己的寄存器,依然会死循环!
}

总结

  • 硬件分配:每个核心拥有绝对私有的 寄存器、L1、L2L3(96MB) 则是所有核心共享。
  • JMM 映射:核心的私有缓存(L1/L2/寄存器)+ 未对外交织的 L3 状态 = JMM 的工作内存
  • Java 的作用:使用 volatile 就是在下一道最高死命令,强制 CPU 停下偷懒、禁止优化、立刻广播,让 9800X3D 那庞大的缓存架构在多线程下整齐划一。

主内存和工作内存

主内存:所有线程共享的内存区域,存放所有的实例变量、静态变量和数组元素(即堆内存和方法区)。 工作内存:每个线程私有的独立内存区域。保存了该线程使用到的变量的主内存副本