@contended能缓解伪共享但默认不生效,因jvm默认禁用,需显式启用-xx:+usecontended;它仅对高竞争实例字段有效,盲目使用会增加内存开销且无收益。

为什么 @Contended 能缓解伪共享但默认不生效
因为 JVM 默认禁用 @Contended,不加启动参数它完全被忽略——连字段对齐、填充字节都不会生成。这不是 bug,是 Oracle 的保守设计:该注解会显著增加对象内存开销,且仅在高竞争、多核密集更新同一缓存行的场景下才值得启用。
- 必须显式开启:
-XX:+UseContended(JDK 8u60+) - 可选调优:
-XX:ContendedPaddingWidth=64控制填充宽度,默认 128 字节;若 CPU 缓存行是 64 字节(主流 x86),设为 64 更精准 - 该注解只对实例字段有效,静态字段、局部变量、数组元素均无效
- 类路径需在
jdk.internal.vm.annotation包下,不能自己仿写——编译能过,运行时 JVM 不识别
@Contended 的正确写法和典型误用
它不是加在类上,也不是用来“隔离整个对象”,而是标注**具体存在竞争风险的字段**。常见错误是把它当成“性能银弹”盲目套用,结果对象体积暴涨却毫无收益。
- 正确姿势:
@Contended private volatile long counter;
- 错误姿势:给整个
ConcurrentHashMap节点类加@Contended——节点里多个字段未必都高竞争,反而浪费 128 字节填充 - 字段类型无关紧要(
int、long、AtomicInteger都可),关键是它是否被多个线程频繁写入且物理地址靠近 - 如果字段本身已用
Unsafe手动对齐或通过Padding字段隔离,再加@Contended会导致重复填充,纯属冗余
不用 @Contended 也能防伪共享的替代方案
多数业务代码其实不需要它。手动填充、合理拆分字段、避免共享可变状态,往往更轻量、更可控。
- 经典 padding 模式:
private volatile long p1, p2, p3, p4, p5, p6, p7; // 7×8=56 字节前置填充 private volatile long value; private volatile long q1, q2, q3, q4, q5, q6, q7; // 后置填充
- 把高频更新字段单独抽成独立对象(如
CounterCell),天然隔离缓存行 - 用
ThreadLocal或分段计数(如LongAdder内部 cell 数组)从源头减少跨线程写竞争 - 注意:JVM 堆压缩、G1 的 region 大小、ZGC 的着色指针都可能影响字段布局,手动 padding 在不同 GC 下效果不一;
@Contended是 JVM 层面统一控制,更稳定但更重
验证伪共享是否真的被解决
别只看代码加了没,得测。伪共享是否缓解,最终体现在多线程更新时的 CAS 失败率、L3 缓存命中率、以及实际吞吐变化上。
立即学习“Java免费学习笔记(深入)”;
- 用 JMH +
-prof perfasm观察热点指令是否还在lock cmpxchg上反复失败 - 用 Linux
perf stat -e cache-misses,cache-references对比加/不加@Contended的缓存未命中率 - 关键指标不是单线程性能——它通常会下降(因对象变大),而是 8 线程并发更新时的吞吐是否提升 20%+;若无明显差异,说明你标的字段压根没构成伪共享
- 容易被忽略的一点:CPU topology。在 NUMA 架构上,即使字段隔离了,若线程绑核不合理(比如所有线程都在同一个 socket 上争同一块 L3),问题依旧存在
真正难的不是加注解,是判断哪个字段值得加、加了有没有用、以及加了之后要不要调 ContendedPaddingWidth。这些没法靠文档猜,得结合 perf 数据和真实负载压测。








