伪共享是CPU缓存行(64字节)导致的性能瓶颈:多个线程高频修改逻辑无关但内存相邻的变量,触发MESI协议下的频繁缓存行失效与重填,造成吞吐量下降;解决需内存隔离,如手动填充或@Contended注解,并用perf或JMH验证。

Java 多线程中,CPU 缓存行和伪共享不是“代码写错”的问题,而是内存布局与硬件协作失配导致的隐形性能瓶颈。核心就一点:**多个线程高频修改逻辑无关的变量,只因它们在内存里挨得太近(同属一个 64 字节缓存行),被 CPU 当作一个整体反复刷新,拖垮吞吐量。**
缓存行是 CPU 读写内存的最小单位
CPU 不按字节、也不按字段读内存,而是按“块”——即缓存行(Cache Line),主流大小为 64 字节。当你访问一个 volatile long 变量,CPU 实际会把包含它的整个 64 字节内存块加载进 L1 缓存。同一缓存行里若还有另一个 volatile long(比如相邻字段),哪怕两个变量完全独立、由不同线程操作,也会被“绑在一起”同步。
例如:
- 一个对象含字段
private volatile long a;和private volatile long b; - JVM 默认按声明顺序紧凑布局,
a占 8 字节,b紧跟其后,两者大概率落在同一缓存行 - 线程 1 改
a→ 触发该缓存行失效 → 线程 2 读/写b时必须重新从主存或 L3 加载整行 → 延迟飙升
伪共享的本质是 MESI 协议下的无效化风暴
现代多核 CPU 用 MESI 等缓存一致性协议保证数据可见性。当一个核心修改某缓存行,会广播“失效”消息,迫使其他核心丢弃自己持有的同一行副本。伪共享发生时:
立即学习“Java免费学习笔记(深入)”;
- 线程 A 在 Core 0 修改字段 X → Core 0 的缓存行变 dirty,并通知 Core 1~N
- 线程 B 在 Core 1 立刻读字段 Y(同缓存行)→ 发现本地副本已失效 → 触发缓存行重填(可能跨插槽走 QPI/UPI 总线)
- 高并发下这种“你改我等、我改你等”的乒乓效应,让大量 CPU 周期花在同步而非计算上
现象表现为:CPU 使用率高、吞吐量不随线程数线性增长、perf stat 显示 cache-misses 占指令比例显著偏高(如 >1%)。
解决伪共享的关键是让敏感字段物理隔离
不能靠加锁或换算法,得从内存布局入手,确保高频更新的字段之间至少间隔 64 字节:
-
手动填充:在字段间插入
private long p1, p2, p3, p4, p5, p6, p7;(7×8=56 字节),配合 8 字节字段自身,凑满 64 字节隔离带 -
@Contended 注解(JDK 8+):
– JDK 8/9 需启动参数:-XX:+UnlockExperimentalVMOptions -XX:+UseContended
– 注解路径必须匹配:@sun.misc.Contended(JDK 8)、@jdk.internal.vm.annotation.Contended(JDK 9+)
– 动态生成类(CGLIB/ByteBuddy)中该注解会被忽略,此时只能回退手动填充 -
避免常见陷阱:数组元素天然连续,
long[]中相邻下标极易伪共享;AtomicLongArray同样存在此风险,需自行对齐或使用 Disruptor 风格的环形缓冲区设计
验证是否真有伪共享,别靠猜
典型误判是把 GC 暂停、锁竞争或 IO 瓶颈当成伪共享。可靠方法只有两个:
-
perf 工具实测:运行
perf stat -e cache-misses,instructions,cpu-cycles -p <jvm-pid>,对比单线程与多线程负载下的 cache-miss/instruction 比值变化 -
JMH 微基准对比:写两个
@Benchmark方法,分别单独操作字段 A 和字段 B;再写一个同时操作两者的版本。若“合跑耗时 ≫ 分跑耗时之和”(尤其差 2~5 倍),且排除 GC 干扰(固定堆、G1 调优),基本可锁定伪共享


















