伪共享(False Sharing)深度解析

5 阅读3分钟

伪共享(False Sharing)深度解析

1. 核心定义

伪共享是指多个线程同时修改位于**同一个缓存行(Cache Line)**中的不同变量,导致 CPU 缓存失效,从而迫使多核 CPU 频繁从内存或 L3 缓存同步数据,产生不必要的性能损耗。


2. 硬件背景:缓存行(Cache Line)

  • 基本单位:CPU 缓存不是按字节存取的,而是以“缓存行”为单位(通常为 64 字节)。
  • 空间局部性:当你访问数组中的一个 AtomicReference 时,CPU 会顺便把相邻的几个引用也加载到同一个缓存行中。

3. 产生原因

线程 A 运行在 Core 1线程 B 运行在 Core 2

  1. 变量分布XY 两个独立的变量物理上紧挨着,处于同一个缓存行。
  2. 并发冲突:线程 A 修改 X,线程 B 修改 Y
  3. 缓存一致性协议 (MESI):虽然 A 和 B 操作的是不同变量,但由于处于同一行,核心 1 对 X 的写操作会导致核心 2 的整个缓存行被标记为 Invalid(无效)
  4. 性能暴跌:核心 2 必须重新加载缓存行,即便它根本没碰过变量 X

4. 解决方案:缓存行填充(Padding)

方案 A:手动填充 (Java 7 及以前)

通过定义无用的 long 变量,确保核心变量在物理上被“隔离”到不同的缓存行。

class PaddedAtomicReference<T> {
    // 前置填充 56 字节 (7 * 8 bytes)
    private var p1: Long = 0; private var p2: Long = 0; private var p3: Long = 0
    private var p4: Long = 0; private var p5: Long = 0; private var p6: Long = 0; private var p7: Long = 0
    
    @Volatile
    var value: T? = null // 实际业务变量
    
    // 后置填充
    private var p8: Long = 0; private var p9: Long = 0; private var p10: Long = 0
    private var p11: Long = 0; private var p12: Long = 0; private var p13: Long = 0; private var p14: Long = 0
}
方案 B:使用 @Contended 注解 (Java 8+)

JVM 自动负责填充,代码更整洁。

// 需要在启动参数中开启 -XX:-RestrictContended
@jdk.internal.vm.annotation.Contended
public class VolatileData {
    public volatile long value = 0L;
}

5. 为什么 Okio 的 SegmentPool 不处理它?

  1. 概率抵消:使用 Thread.currentThread().id 进行 Hash 散列,活跃线程撞在物理相邻桶上的概率极低。
  2. 权衡利弊:填充会带来巨大的内存浪费(每个桶膨胀到 64 字节以上),在移动端环境下,内存比这点极细微的 CPU 抖动更宝贵。
  3. 操作频率Segment 的分配不是纳秒级的超高频操作,I/O 等待才是性能大头。

6. 总结对比

特性普通并发伪共享场景优化后 (Padding)
并发单位逻辑变量物理缓存行独占缓存行
同步代价低 (CAS)极高 (强制同步内存)极低 (独立缓存)
内存开销极小极小较大

面试 Tip:如果面试官问你如何优化一个高并发计数器,除了说 LongAdder,提到“防止伪共享”会让他觉得你对硬件层面的并发优化有非常深入的理解。