Appearance
什么是伪共享问题以及如何解决
伪共享(False Sharing)是多线程编程中的一个重要性能问题,尤其在多核处理器中尤为显著。了解如何因共享缓存行引起的无谓性能消耗,从而有效规避,是编写高效并发程序的关键。
伪共享的核心概念
在现代处理器中,缓存行(Cache Line)是缓存的最小可分配单位,通常是64字节。当多个线程在不同CPU核心上操作缓存行中的不同变量时,如果这些变量位于同一个缓存行,修改其中一个变量会导致整个缓存行被标记为无效(cpu缓存一致性决定)。这种重复的缓存行无效化和重新加载的现象就是伪共享。
它的本质问题在于:虽然多个线程操作的是物理上不同的数据,但由于它们共享了同一个缓存行,造成不必要的缓存同步,导致性能下降。
示例代码解释
为了更好地理解伪共享,我们结合上面的代码来详细解释:
java
public class FalseSharingExample implements Runnable {
// 定义线程数
public static int NUM_THREADS = 4;
public static final long ITERATIONS = 5000000000L; // 每个线程的迭代次数
private final int arrayIndex; // 此线程操作的共享数据索引
private static ValuePadding[] values; // 用于存储共享数据
public FalseSharingExample(int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
// 初始化共享变量数组
values = new ValuePadding[NUM_THREADS];
for (int i = 0; i < values.length; i++) {
values[i] = new ValuePadding();
}
final long start = System.nanoTime();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharingExample(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("Duration = " + (System.nanoTime() - start));
}
@Override
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
// 更新当前线程对应的共享变量
values[arrayIndex].value = i;
}
}
// 内部类,带有缓存行填充以避免伪共享
public static class ValuePadding {
protected long p1, p2, p3, p4, p5, p6, p7; // 填充字段
public volatile long value = 0L; // 实际操作的变量
protected long p9, p10, p11, p12, p13, p14, p15; // 填充字段
}
// @Contended注释的使用演示,如果使用该注解,需要加启动参数启用 -XX:-RestrictContended
/*
public static class ValueWithContended {
@Contended
public volatile long value = 0L;
}
*/
}代码的详细解释
**ValuePadding**** **类的设计:- 这里使用了
ValuePadding类来存储每个线程要操作的共享变量value。 - 通过在
value字段的前后添加几个填充字段(p1到p7和p9到p15),每个ValuePadding对象会占据多个缓存行,从而使value与相邻的对象实际分离至不同的缓存行。 - 这可以有效防止多个线程不必要地共享同一缓存行(即避免伪共享),从而提升程序的并发性能。
- 这里使用了
- 主程序的执行流程:
- 主线程启动了多个工作线程,每个线程负责增加自己的
ValuePadding实例中的value。 - 每个线程访问和更新
values数组中的一个ValuePadding实例,由于填充字段的存在,每个线程访问的value字段分布在不同缓存行上,尽可能减少缓存争用。
- 主线程启动了多个工作线程,每个线程负责增加自己的
- 代码运行效率:
- 如果不采用填充策略,不同线程可能会因共享同一缓存行而导致频繁的缓存无效化和重新加载,造成性能的严重下降。
- 通过填充确保每个
value字段在不同缓存行上,甚至在高并发下也能高效执行,从而减少伪共享。
总结
伪共享问题的解决尤其适合在性能敏感的大规模并发应用中。通过了解缓存行的基本原理并采取适当的填充策略,可以有效减少缓存行争用带来的性能开销,从而在不改变逻辑操作的前提下实现更佳的性能。注意,Java8还提供@Contended注解用于更高级的填充,但使用它需要相应的JVM参数配置支持。
更新: 2024-08-12 14:07:26
原文: https://www.yuque.com/tulingzhouyu/db22bv/iy0whbbdmwbzgoei