一、定义与本质
伪共享是指多个线程同时读写不同变量,但这些变量恰好位于同一个缓存行(Cache Line)中,
导致缓存失效的现象。本质是缓存行层面的资源竞争,虽不直接冲突,但会间接影响性能。
二、CPU 缓存架构与缓存行
-
多级缓存结构:
- L1(最快,每个核心独享)→ L2(核心独享)→ L3(多核共享)→ 主存(最慢)。
- 缓存与主存的数据交换以 缓存行(通常 64 字节)为单位。
-
缓存行机制:
- 当 CPU 读取一个变量时,会将整个缓存行载入缓存。
- 若缓存行中其他变量被修改,整个缓存行需失效(Invalidate)并重新从主存加载。
三、伪共享的危害
示例场景:
class Counter {
long countA; // 线程A修改
long countB; // 线程B修改
}
-
若
countA和countB位于同一缓存行:- 线程 A 修改
countA,导致缓存行失效。 - 线程 B 读取
countB时需重新加载整个缓存行,即使countB未被修改。 - 性能下降:频繁的缓存行失效和重新加载(称为 缓存颠簸)。
- 线程 A 修改
四、解决方法:缓存行填充(Padding)
通过填充使变量独占缓存行,避免被其他变量影响。
1. 传统手动填充(JDK 8 前)
class Counter {
long p0, p1, p2, p3, p4, p5, p6; // 前填充
long countA; // 独占缓存行
long q0, q1, q2, q3, q4, q5, q6; // 后填充
long countB; // 另一个缓存行
}
- 原理:每个
long占 8 字节,7 个填充变量 + 1 个数据变量 = 64 字节(刚好 1 个缓存行)。
2. JDK 8 的 @sun.misc.Contended 注解
@sun.misc.Contended
class CounterCell {
volatile long value;
}
- 原理:JVM 自动在变量前后添加填充,确保独占缓存行。
- 启用参数:需添加 JVM 参数
-XX:-RestrictContended。
五、典型应用案例
-
JDK 中的实现:
ConcurrentHashMap的CounterCell使用@Contended避免伪共享。LongAdder、Striped64等并发组件同样依赖此优化。
-
高性能框架:
- Netty 的
HashedWheelTimer使用手动填充确保时间轮的槽位独占缓存行。
- Netty 的
六、性能对比测试
测试代码(简化版) :
// 无填充
class Data {
long value1;
long value2;
}
// 有填充
@Contended
class PaddedData {
long value1;
long value2;
}
// 测试逻辑:双线程分别修改 value1 和 value2
测试结果示例:
| 场景 | 操作耗时 |
|---|---|
| 无填充 | 1200 ns |
| 手动填充 | 150 ns |
@Contended | 120 ns |
七、注意事项
-
过度使用的代价:
- 填充增加内存消耗(每个变量独占 64 字节)。
- 可能导致缓存利用率下降(如缓存行浪费)。
-
现代处理器的优化:
- 部分处理器(如 Intel)支持 MESI 协议 的增强版(如 MESIF),可减少伪共享影响。
- 但在高并发场景下,手动优化仍有必要。
八、总结
伪共享是多线程编程中隐蔽的性能杀手,通过合理的缓存行填充(手动或注解)可显著提升性能。理解伪共享需掌握:
-
缓存行机制:CPU 以行为单位读写缓存。
-
竞争模式:不同变量在同一缓存行时的间接竞争。
-
优化手段:填充技术与
@Contended注解的应用。
在设计高性能并发组件(如计数器、时间轮、队列)时,伪共享是必须考虑的优化点。