CPU 缓存架构与多核可见性问题
并发基础 · 第 5/5 篇
上一篇:《原子性、可见性、有序性与 JMM》 · 下一系列:《Callable、Future 与 FutureTask》
开头:JMM 的「工作内存」在硬件上是什么?
上一篇说线程各有工作内存副本——在真实机器上,这 largely 对应 CPU 多级缓存 与写缓冲。多核各有一份缓存,若不同步,就会出现 JMM 层面的可见性问题;若同一缓存行被无关变量共享,还会出现 伪共享 拖垮性能。
本篇串联:高速缓存 → 多核一致性 → MESI → 伪共享与规避,并简要说明这与高性能队列设计的关系。

一、CPU 高速缓存
Cache 位于 CPU 与主存之间,容量小、速度快。常见 L1 / L2 / L3;每一级缓存存储的数据都是下一级的子集,技术难度与制造成本递减、容量递增。
CPU 速度远高于主存,直接从内存读写需等待多个时钟周期。Cache 保存 CPU 刚用过或循环使用的一部分数据,再次使用时可直接从 Cache 读取,减少等待。
局部性原理
- 时间局部性(Temporal):刚访问的信息很可能再次访问——循环、递归、方法反复调用。
- 空间局部性(Spatial):某地址被引用,附近地址也很可能被引用——顺序代码、数组、连续分配的对象。
多级缓存与寄存器
现代 CPU 集成多级缓存。CPU 寄存器位于 CPU 内部,速度最快、容量极小(几十到几百字节),常用数据优先放寄存器。
读路径:L1 → L2 → L3 → 主内存。
写路径:先写 L1,再按缓存一致性协议决定是否写 L2/L3/主存(可能写回、可能使其它核副本失效)。
多核下的问题:
- 场景一:核 A 修改共享数据,核 B 仍用旧副本 → 一致性问题。
- 场景二:两线程改同一 Cache Line 内不同字段 → 频繁失效,伪共享。
二、缓存一致性的硬件手段
IA-32 手册描述,处理器用三种相互依赖的机制执行锁定的原子操作:
- 保证的原子操作(如
XADD、XCHG、CMPXCHG) - 总线锁定(
LOCK#信号与LOCK指令前缀) - 缓存锁定 / 一致性协议(Pentium 4、Xeon、P6 及以后主流)
何时退化为总线锁:
- 数据不能被缓存在处理器内部(如部分设备内存)
- 操作跨多个 Cache Line
现代处理器应尽量用缓存锁定实现原子操作,性能更好。早期 Pentium 不支持缓存锁定,只能总线锁。
三、总线窥探与 MESI
3.1 总线窥探(Bus Snooping)
一致性控制器监视总线事务,当共享缓存块被修改时,通知其它持有副本的缓存失效(invalidate)或更新(update)。
窥探协议类型:
| 类型 | 行为 | 代表 |
|---|---|---|
| 写失效(Write-invalidate) | 写时使其它副本失效 | MSI、MESI、MOESI 等(最常用) |
| 写更新(Write-update) | 写时广播更新到所有缓存 | Dragon、Firefly(总线流量大,较少见) |
3.2 MESI 协议
MESI(又称 Illinois 协议)是基于写失效、支持回写(write-back)缓存的最常用协议。
| 状态 | 含义 |
|---|---|
| M(Modified) | 脏数据,与主存不一致;其它核读主存前须回写 |
| E(Exclusive) | 独占且干净,与主存一致 |
| S(Shared) | 多核共享且干净 |
| I(Invalid) | 无效 |
Cache-to-cache 复制:miss 时若其它核已有 Shared 副本,可直接复制,减少访存;但 M 状态须先写回主内存再复制,保证一致。
四、伪共享(False Sharing)
多个核上的线程操作同一 Cache Line 内不同变量,仍会频繁使对方缓存失效——代码层面变量无关,硬件上却互相干扰,即伪共享。
典型例子:ArrayBlockingQueue 的 takeIndex、putIndex、count 易落在同一 64 字节行内。生产者 put 改 putIndex 会使消费者核上的缓存行失效,反之亦然。
查看 Cache Line 大小(Linux,通常 64 字节):
getconf LEVEL1_DCACHE_LINESIZE
# 或 cat /proc/cpuinfo规避方案
方案 1:缓存行填充
class Pointer {
volatile long x;
long p1, p2, p3, p4, p5, p6, p7; // 填充,使 x 与 y 不在同一行
volatile long y;
}方案 2:@sun.misc.Contended(JDK 8+)
常配合 JVM 参数 -XX:-RestrictContended 使用。
方案 3:ThreadLocal——变量不跨线程共享,从根源避免行竞争。
Benchmark 现象:未填充时两线程各自增 x、y,耗时远高于填充后(同一定义下的 Pointer 对比实验)。
五、从缓存问题到高性能队列(延伸)
JUC 有界队列(如 ArrayBlockingQueue)多用 ReentrantLock:
- 高稳定系统为防止生产者过快导致 OOM,常选有界队列。
- 加锁带来竞争、上下文切换与死锁风险。
- 数组实现又易触发伪共享。
LMAX Disruptor 针对内存队列延迟问题设计(单线程每秒数百万订单级别),核心思路包括:
- 环形数组(2^n 长度,位运算定位,O(1) 存取)
- CAS 无锁(或极少锁)序列号申请 slot
- 缓存行填充隔离 hot 字段
- 事件驱动生产者-消费者(观察者模式)
Log4j 2 异步模式采用 Disruptor;64 线程下吞吐量可比 Async Appender 高一个数量级。Disruptor 细节见专栏 性能扩展 篇;此处只需建立联系:懂 CPU 缓存,才理解为何要 padding、为何无锁队列要关心 Cache Line。
小结
- JMM 工作内存 在硬件上映射为 per-core 缓存与缓冲。
- MESI + 窥探 保证多核一致;伪共享 是性能杀手。
- 工程上:
Contended、填充、布局拆分、ThreadLocal 按场景选用。
并发基础系列至此收束;下一系列进入 异步编程:Future → CompletableFuture → ThreadLocal。