面试知识库
困难

伪共享与缓存行#

一句话答案#

不同核心修改同一缓存行(64B)中不同变量导致频繁失效,解决:@Contended 注解或手动填充使变量独占缓存行。

核心要点

CPU 缓存行(Cache Line):

  • CPU 读取内存的最小单位是缓存行(通常 64 字节)
  • 一次加载会把相邻数据一起加入缓存

伪共享问题:

Cache Line (64 bytes): [varA | varB | padding...]
                        ↑         ↑
                     Thread1    Thread2
plaintext
  • Thread1 修改 varA → 整行失效 → Thread2 的 varB 缓存也失效
  • Thread2 修改 varB → 整行失效 → Thread1 的 varA 缓存也失效
  • 结果:频繁缓存失效,性能骤降

解决方案:

  1. @Contended 注解(JDK8+):自动填充128字节
    @sun.misc.Contended
    class PaddedAtomicLong extends AtomicLong {}
    java
    需要 -XX:-RestrictContended
  2. 手动填充:在变量前后加 long 字段填满一行
  3. 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 好——后者更可靠