问答题1013/1053了解过什么是“伪共享”吗?

难度:
2021-11-02 创建

参考答案:

伪共享(False Sharing) 是一种性能问题,通常发生在多线程并发环境中,尤其是在现代多核处理器上。它的产生源于 CPU 的缓存一致性协议(如 MESI 协议),并会导致多线程程序的性能严重下降。

伪共享的定义:

伪共享发生在多个线程操作不同的变量时,这些变量虽然不直接相关,但它们恰好位于同一缓存行内。由于 CPU 在缓存中存储数据时是按缓存行(通常是 64 字节)来存储的,多个变量如果位于同一个缓存行中,即使它们是不同的变量,也会导致缓存行频繁地在多个 CPU 核心之间失效,进而影响性能。

具体来说,当多个线程同时访问同一个缓存行中的不同变量时,尽管这些线程并没有直接竞争资源,但缓存一致性协议会确保所有线程访问的数据是一致的,这就导致了频繁的缓存同步和无意义的数据传输,浪费了 CPU 的缓存带宽,最终降低了程序的性能。

伪共享的工作原理:

假设有两个线程分别操作不同的变量,且这些变量恰好位于同一个缓存行中:

  • 线程 A 修改变量 counter1,线程 B 修改变量 counter2
  • 即使 counter1counter2 是独立的,位于同一缓存行中,CPU 仍然会认为它们是相关的,并通过缓存一致性协议(如 MESI 协议)不断更新它们在各个缓存中的副本。
  • 这会导致缓存失效,频繁地将缓存行从一个 CPU 的缓存中清除并更新到另一个 CPU 的缓存中,造成不必要的性能损失。

伪共享的示例:

1public class FalseSharingExample implements Runnable { 2 private static final int NUM_THREADS = 2; 3 private static final int NUM_UPDATES = 1000000; 4 private static VolatileLong[] counters = new VolatileLong[NUM_THREADS]; 5 6 static { 7 for (int i = 0; i < NUM_THREADS; i++) { 8 counters[i] = new VolatileLong(); 9 } 10 } 11 12 public static void main(String[] args) throws InterruptedException { 13 Thread[] threads = new Thread[NUM_THREADS]; 14 for (int i = 0; i < NUM_THREADS; i++) { 15 threads[i] = new Thread(new FalseSharingExample()); 16 threads[i].start(); 17 } 18 19 for (int i = 0; i < NUM_THREADS; i++) { 20 threads[i].join(); 21 } 22 23 long sum = 0; 24 for (VolatileLong counter : counters) { 25 sum += counter.value; 26 } 27 System.out.println("Sum of counters: " + sum); 28 } 29 30 @Override 31 public void run() { 32 for (int i = 0; i < NUM_UPDATES; i++) { 33 counters[Thread.currentThread().getId() % NUM_THREADS].value++; 34 } 35 } 36} 37 38class VolatileLong { 39 // 64-byte cache line 40 public volatile long value; 41}

在上述代码中,VolatileLong 对象的 value 字段会被多个线程频繁地更新。假如多个 VolatileLong 实例恰好位于同一缓存行内,线程在访问不同 VolatileLong 实例时就会产生伪共享,导致性能下降。

如何避免伪共享:

  1. 使用填充(Padding):通过在字段之间加入无用的空字段(例如 long 类型),来确保变量不在同一缓存行内。

    1class PaddedVolatileLong { 2 // 填充字段确保变量不在同一缓存行内 3 public volatile long value; 4 public volatile long padding1, padding2, padding3; 5}
  2. 使用 @Contended 注解:从 Java 8u40 开始,JVM 提供了 @Contended 注解(需要启用特定的 JVM 参数),可以避免字段放在同一个缓存行内。该注解会增加额外的填充,确保字段不会共享缓存行。

    1import sun.misc.Contended; 2 3class NoFalseSharing { 4 @Contended 5 public volatile long counter1; 6 @Contended 7 public volatile long counter2; 8}
  3. 合理排列字段:通过手动调整字段的顺序,使得多个线程访问的变量不位于同一缓存行内,从而避免伪共享。

  4. 使用原子变量java.util.concurrent.atomic 包中的原子类(如 AtomicLong)采用了适当的内存布局,减少了伪共享的可能性。

最近更新时间:2024-12-06