多线程修改同一缓存行会因MESI协议频繁失效缓存导致伪共享,性能下降2–5倍;典型表现为高CPU低吞吐、JMH结果异常波动,可通过JOL查看布局、perf观测缓存未命中验证。

为什么多线程改同一缓存行会变慢
CPU 读写内存不是按字节,而是按缓存行(通常 64 字节)。当两个线程频繁修改位于同一缓存行的不同变量时,即使它们互不干扰,也会因缓存一致性协议(如 MESI)反复使对方的缓存行失效——这就是伪共享。性能下降可能达 2–5 倍,且毫无明显报错。
常见错误现象:volatile 变量或 AtomicInteger 字段密集更新时吞吐骤降、jstack 看不到阻塞但 CPU 持续跑满、JMH 基准测试结果波动大且远低于理论值。
- 典型场景:环形缓冲区(RingBuffer)的
head和tail指针紧挨着定义;并发计数器数组中相邻元素被不同线程更新 - 验证方式:用
java -XX:+PrintAssembly或perf cache-misses观察缓存未命中率突增;更直接的是用 JOL(Java Object Layout)查看字段内存布局:new UnsafeCounter().getClass()+ClassLayout.parseClass(...).toPrintable() - 注意:对象头、对齐填充、字段重排序都影响实际布局,
javac不保证字段声明顺序 = 内存顺序
@Contended 注解怎么起作用
@Contended 是 JDK 8 引入的、用于隔离字段组的注解,本质是让 JVM 在该字段前后插入 128 字节填充(默认),确保它独占至少一个缓存行。但它默认不生效,必须显式开启 VM 参数。
使用前提:-XX:+UnlockExperimentalVMOptions -XX:+UseContended,缺一不可。JDK 9+ 默认启用 UseContended,但老版本仍需手动开。
立即学习“Java免费学习笔记(深入)”;
- 仅对实例字段有效,静态字段加了也无效
- 字段必须是
private(JDK 8)、protected或包级,public字段会被忽略 - 可带分组名:
@Contended("counter"),同组字段会被打包到一起并整体填充,适合逻辑关联的多个字段 - 示例:
@Contended private volatile long value;
编译后,value字段前后将插入填充字段,避免与前/后字段共用缓存行
不用 @Contended 怎么手动规避伪共享
手动填充是兼容性最强的方式,尤其在不能加 JVM 参数的老环境(如某些容器化部署、Android ART、低版本 JDK)中必须用。
核心思路:用无用字段“撑开”目标字段,使其前后至少空出 64 字节(保守起见常用 128 字节)。
- 推荐结构:在目标字段前后各放 7 个
long(7×8=56 字节),再加一个byte对齐,凑够 64 字节边界;或者直接用long p0, p1, ..., p7占位 - 别用
Object或数组占位——它们本身有对象头,大小不可控;也别依赖serialVersionUID等“看起来没用”的字段,JVM 可能优化掉 - 示例:
class PaddedCounter { private volatile long p0, p1, p2, p3, p4, p5, p6; private volatile long value; private volatile long q0, q1, q2, q3, q4, q5, q6; }这样value基本不会和任何其他字段落在同一缓存行 - 注意:Lombok 的
@Data会生成toString(),若包含大量填充字段,打印日志时可能拖慢调试——生产环境建议关掉或过滤
伪共享问题容易被误判成什么
伪共享没有异常、不抛 Exception、不触发 GC 日志,很容易被当成“线程调度问题”“锁粒度太粗”甚至“硬件故障”来排查。
- 最常被混淆的是 synchronized 锁竞争:看到线程 BLOCKED 就加锁优化,但其实线程根本没抢锁,只是在疯狂同步缓存行
- GC 日志里
ParNew时间长?可能是伪共享导致 Mutator 线程卡顿,间接拉长 GC STW 时间,但根源不在堆上 - JFR(Java Flight Recorder)里重点关注
CompilerInlining和CacheLineInvalidations事件(需开启-XX:+FlightRecorder -XX:StartFlightRecording=settings=profile) - 真正关键的信号是:单线程性能正常,2 线程就掉 30%,4 线程掉 60% 以上,且线程数增加与性能下降呈非线性关系
缓存行对齐这事,看着是内存布局问题,实际是 CPU 和 JVM 共同决定的执行路径。字段位置差几个字节,性能可能差一个数量级——而这个差值,在源码里完全看不出来。










