中 困难
伪共享与缓存行#
一句话答案#
不同核心修改同一缓存行(64B)中不同变量导致频繁失效,解决:@Contended 注解或手动填充使变量独占缓存行。
核心要点
CPU 缓存行(Cache Line):
- CPU 读取内存的最小单位是缓存行(通常 64 字节)
- 一次加载会把相邻数据一起加入缓存
伪共享问题:
Cache Line (64 bytes): [varA | varB | padding...]
↑ ↑
Thread1 Thread2plaintext- Thread1 修改 varA → 整行失效 → Thread2 的 varB 缓存也失效
- Thread2 修改 varB → 整行失效 → Thread1 的 varA 缓存也失效
- 结果:频繁缓存失效,性能骤降
解决方案:
- @Contended 注解(JDK8+):自动填充128字节
需要
java@sun.misc.Contended class PaddedAtomicLong extends AtomicLong {}-XX:-RestrictContended - 手动填充:在变量前后加 long 字段填满一行
- Disruptor RingBuffer 的
Sequence就用了填充避免伪共享
面试回答(2分钟版)
伪共享是多核 CPU 环境下的性能隐患。CPU 读取内存的最小单位是缓存行,通常 64 字节,一次加载会把相邻的数据一起拉入缓存。当两个线程运行在不同 CPU 核心上,各自频繁修改的变量恰好落在同一个缓存行中时,就会触发伪共享:线程 A 修改变量 a 导致整行缓存失效,线程 B 的变量 b 虽然没被修改但也必须重新从内存加载,反之亦然。两个核心来回使对方的缓存失效,性能可能下降数倍。解决办法是让热点变量独占一个缓存行:JDK 8 提供了 @sun.misc.Contended 注解,加上后 JVM 自动在变量前后填充 128 字节的 padding,需要搭配 -XX:-RestrictContended 参数使用;也可以手动在变量前后声明多个 long 字段填满 64 字节。实际案例中 Disruptor 的 Sequence、LongAdder 的 Cell 都使用了缓存行填充来避免伪共享,这在高并发计数器场景下性能提升非常显著。
追问与易错
追问方向:
- “怎么验证伪共享存在?”→ 用 JMH 基准测试对比填充前后的吞吐量差异,或使用 perf/Linux perf stat 观察 L1 cache miss 次数,伪共享时 cache miss 会显著偏高
- “Java 中还有哪些组件用了缓存行填充?”→ LongAdder 的 Cell 类使用 @Contended、Disruptor 的 Sequence、ConcurrentHashMap 的 CounterCell、ForkJoinPool 的 WorkQueue
- “@Contended 需要什么 JVM 参数?”→ 需要 -XX:-RestrictContended 参数,默认该注解仅对 JDK 内部类生效,用户类必须加此参数才能启用填充
易错点:
- ❌ 伪共享很少发生——高并发计数器场景很常见
- ❌ 手动填充比 @Contended 好——后者更可靠