简述伪共享的概念以及如何避免
伪共享(False Sharing)是多线程并发编程中的一种性能隐患,常出现在使用多核 CPU 进行共享内存访问时。虽然不会导致程序错误,但会严重影响性能,尤其在高频写操作下。
一、什么是伪共享(False Sharing)?
定义:
多个线程访问不同变量,但这些变量共享了同一个缓存行(cache line),会导致频繁的缓存同步,产生性能瓶颈。
通俗理解:
- CPU 缓存是以 缓存行(cache line) 为单位读写的(通常为 64 字节)。
- 如果多个线程访问的不同变量恰好落在同一个 cache line 上:
- 即使操作不同变量,仍会导致缓存失效(cache invalidation)。
- 多个 CPU 频繁地进行缓存一致性同步(MESI 协议),导致性能下降。
二、为什么会有伪共享问题?
Java 中一个对象包含多个字段时,它们可能紧密排列在内存中,甚至多个对象也可能在数组里紧邻排布。
当不同线程修改这些字段(即便不是同一个字段),只要它们落在同一缓存行,就会引发伪共享问题。
三、伪共享示例(Java)
public class FalseSharingDemo {
static class Data {
public volatile long value = 0L;
}
static final int NUM_THREADS = 4;
static final long ITERATIONS = 10_000_000L;
static final Data[] datas = new Data[NUM_THREADS];
static {
for (int i = 0; i < NUM_THREADS; i++) {
datas[i] = new Data();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
final int index = i;
threads[i] = new Thread(() -> {
for (long j = 0; j < ITERATIONS; j++) {
datas[index].value = j;
}
});
}
long start = System.nanoTime();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
long end = System.nanoTime();
System.out.println("Duration: " + (end - start) / 1_000_000 + " ms");
}
}
🔺 问题:
所有 value 字段可能被分配在同一个缓存行中,多线程同时写入不同元素,依然会互相干扰。
四、如何避免伪共享?
方法 1:使用缓存行填充(padding)
手动添加无用字段,使真正要用的字段不共享同一 cache line。
class PaddedData {
public volatile long value = 0L;
// 添加填充字段,避免伪共享
public long p1, p2, p3, p4, p5, p6, p7;
}
每个字段占 8 字节,添加 7 个即可填满 64 字节缓存行。
方法 2:使用 JDK 8 的 @Contended 注解(推荐方式)
JDK 8+ 提供了一个专门用于避免伪共享的注解:
import jdk.internal.vm.annotation.Contended;
public class ContendedData {
@Contended
public volatile long value;
}
📌 需要 JVM 参数开启:
-XX:-RestrictContended
方法 3:使用数组分配间隔空间(不推荐)
人为在数组中加间隔项,比如:
datas[i * 16] = new Data();
让实际访问项距离足够远,但这种方式依赖具体实现,不可靠。
方法 4:使用高性能并发类(如 LongAdder)
JDK 提供了 LongAdder 等专门优化的类,它们内部使用分段 + padding技术避免伪共享。
适用于高并发场景下频繁加法统计:
LongAdder adder = new LongAdder();
adder.increment(); // 比 AtomicLong 性能高
五、伪共享 vs 真共享
| 类型 | 说明 |
|---|---|
| 真共享 | 多线程同时访问同一个变量 |
| 伪共享 | 多线程访问不同变量,但在同一 cache line 内 |
| 无共享 | 多线程各自独立访问完全不同的 cache line 数据 |
六、参考资料与官方出处
- OpenJDK @Contended 官方文档
- 《Java 并发编程实战》 – Brian Goetz(推荐书籍)
- Martin Thompson: False Sharing in Java
- JEP 142: Eliminate False Sharing with @Contended
七、总结(面试 & 实战)
| 问题 | 简洁回答 |
|---|---|
| 什么是伪共享? | 多线程访问不同变量,但变量共享同一缓存行,导致性能下降 |
| 为什么会慢? | 缓存一致性协议导致频繁无效同步(MESI 协议) |
| 怎么避免? | 使用 padding、@Contended、或高并发类如 LongAdder |
| 实际影响? | 在高并发写操作中,性能可能下降 5~30 倍 |
更多详细内容请关注其他相关文章!