伪共享(False Sharing)深度解析
1. 核心定义
伪共享是指多个线程同时修改位于**同一个缓存行(Cache Line)**中的不同变量,导致 CPU 缓存失效,从而迫使多核 CPU 频繁从内存或 L3 缓存同步数据,产生不必要的性能损耗。
2. 硬件背景:缓存行(Cache Line)
- 基本单位:CPU 缓存不是按字节存取的,而是以“缓存行”为单位(通常为 64 字节)。
- 空间局部性:当你访问数组中的一个
AtomicReference时,CPU 会顺便把相邻的几个引用也加载到同一个缓存行中。
3. 产生原因
当 线程 A 运行在 Core 1,线程 B 运行在 Core 2:
- 变量分布:
X和Y两个独立的变量物理上紧挨着,处于同一个缓存行。 - 并发冲突:线程 A 修改
X,线程 B 修改Y。 - 缓存一致性协议 (MESI):虽然 A 和 B 操作的是不同变量,但由于处于同一行,核心 1 对
X的写操作会导致核心 2 的整个缓存行被标记为 Invalid(无效)。 - 性能暴跌:核心 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 不处理它?
- 概率抵消:使用
Thread.currentThread().id进行 Hash 散列,活跃线程撞在物理相邻桶上的概率极低。 - 权衡利弊:填充会带来巨大的内存浪费(每个桶膨胀到 64 字节以上),在移动端环境下,内存比这点极细微的 CPU 抖动更宝贵。
- 操作频率:
Segment的分配不是纳秒级的超高频操作,I/O 等待才是性能大头。
6. 总结对比
| 特性 | 普通并发 | 伪共享场景 | 优化后 (Padding) |
|---|---|---|---|
| 并发单位 | 逻辑变量 | 物理缓存行 | 独占缓存行 |
| 同步代价 | 低 (CAS) | 极高 (强制同步内存) | 极低 (独立缓存) |
| 内存开销 | 极小 | 极小 | 较大 |
面试 Tip:如果面试官问你如何优化一个高并发计数器,除了说
LongAdder,提到“防止伪共享”会让他觉得你对硬件层面的并发优化有非常深入的理解。