伪共享是因变量在内存中“站得太近”导致的性能问题:两个线程各自修改独立volatile long,却因共享同一64字节缓存行而被硬件强制同步,吞吐量常下降3–8倍;需用JOL实测字段偏移差是否≤64,手动填充常用7个long(56字节)确保独占缓存行,或启用@Contended注解并配JVM参数。

假共享不是代码写错了,而是变量在内存里“站得太近”——两个线程各自改自己的 volatile long,却因共享同一缓存行被硬件强制同步,吞吐掉 3–8 倍很常见。
怎么看变量是不是挤在同一个缓存行里
Java 对象字段的内存布局不等于源码声明顺序,javac 和 JVM 可能重排、填充、对齐。光看类定义无法判断是否伪共享。
- 用
JOL(Java Object Layout)实测:运行ClassLayout.parseClass(Data.class).toPrintable(),观察x和y的 offset 差值是否 ≤64 - 注意对象头(12 字节)、字段对齐(通常按 8 字节边界)、以及 JVM 默认的字段排序策略(long/double 优先靠前)都会干扰结果
- 不要依赖 IDE 的“结构预览”或反编译,那些不反映真实堆内存布局
手动填充(Padding)时为什么常用 7 个 long
目标是让关键字段前后至少空出 64 字节,确保它独占一个缓存行。64 − 8(一个 long 占 8 字节) = 56,而 7 × 8 = 56 —— 这是最小有效填充量。
- 填 6 个
long(48 字节)不够:前/后可能仍有其他字段侵入同一缓存行 - 填 8 个会浪费内存,且不一定提升效果;实际中 7 个已覆盖主流 x86(64B)和多数 ARM(64B)场景
- 如果部署在 Apple M 系列(128B 缓存行),需填 15 个
long(15×8=120),但此时更建议用@Contended或调整 JVM 参数 - 字段必须是
private或包级(JDK 8),public填充字段会被 JVM 忽略
@Contended 注解为什么开了参数也不生效
这个注解默认被禁用,且只对实例字段起作用,静态字段加了也白加。
立即学习“Java免费学习笔记(深入)”;
- 必须同时启用两个 JVM 参数:
-XX:+UnlockExperimentalVMOptions -XX:+UseContended(JDK 8/9);JDK 10+ 后者默认开启,但前者仍要加 - 注解类路径在 JDK 8 是
@sun.misc.Contended,JDK 9+ 是@jdk.internal.vm.annotation.Contended,混用会编译通过但运行时无效 - 如果类被
ClassLoader.defineClass动态生成(如某些字节码增强框架),@Contended可能被丢弃,此时只能退回到手动填充 - 填充宽度可调:
-XX:ContendedPaddingWidth=128,但该值必须是 2 的幂,且不能小于缓存行大小
环形缓冲区(RingBuffer)里 head 和 tail 怎么避免假共享
Disruptor 的经典设计不是靠单个对象填充,而是把高频更新字段彻底隔离到不同对象中,从根本上切断缓存行耦合。
- 不要把
head和tail放在同一个类里,哪怕加了@Contended—— 它们仍可能被分配到相邻内存块,尤其在 GC 后内存紧凑时 - 正确做法:每个字段封装进独立的单字段类(如
Sequence),再用Unsafe或VarHandle直接操作其地址,绕过 JVM 字段访问逻辑 - 数组场景更危险:
AtomicIntegerArray中索引 0 和 1 几乎必然同缓存行,应改用分段数组(如每 16 个元素一组,组间 padding)或线程局部计数器聚合 - 验证是否真正解决:用
perf stat -e cache-misses,cache-references对比,缓存未命中率下降 30%+ 才算有效
最易被忽略的一点:伪共享问题在低并发下几乎不可见,压测时若只跑单线程或两线程,很可能漏掉;必须用 ≥CPU 核心数的线程持续争抢,才能暴露缓存行竞争的真实开销。


















