问答题1011/1053如何避免“伪共享”?

难度:
2021-11-02 创建

参考答案:

伪共享(False Sharing) 是一种性能问题,发生在多个线程同时访问同一个缓存行时,导致 CPU 缓存不必要地频繁失效,进而引发性能下降。伪共享通常发生在不同线程修改不同变量时,这些变量恰巧位于同一个缓存行内。尽管这些变量是独立的,但它们共享同一缓存行,导致缓存一致性协议(如 MESI)频繁地将缓存行从一个 CPU 缓存中清除并更新到另一个 CPU 缓存中,浪费了大量的 CPU 缓存带宽和时间。

伪共享的原因

  • 多个线程访问的变量在内存中距离非常近,位于同一个缓存行内。
  • 即使这些变量的访问是独立的,缓存一致性协议仍然会导致这些线程频繁地将缓存行标记为“脏”状态并传播更新,从而产生性能瓶颁。

如何避免伪共享

避免伪共享的关键是确保多个线程访问的变量不共享同一个缓存行。这可以通过以下几种方法实现:

1. 使用 @Contended 注解(JVM)

  • 从 Java 8u40 开始,JVM 提供了 @Contended 注解(需要开启特定的 JVM 参数),可以指示 JVM 对类字段进行填充,确保它们不会在同一个缓存行内。
  • 这个注解会增加字段间的填充,避免它们被放在同一个缓存行内。使用 @Contended 注解时,需要通过 -XX:-RestrictContended 参数启用此功能。
1import sun.misc.Contended; 2 3public class MyClass { 4 @Contended 5 private long counter1; 6 7 @Contended 8 private long counter2; 9}
  • 注意:此注解依赖于 JDK 内部 API,因此仅在支持的版本中可用。

2. 手动填充(Padding)

  • 通过手动插入填充变量(如 longint 等),可以将共享变量之间增加足够的间隔,使它们位于不同的缓存行内。缓存行的大小通常是 64 字节,因此可以通过调整字段的大小和顺序来避免多个变量位于同一个缓存行。
  • 例如,可以通过插入 long 类型的间隔来确保每个变量占据独立的缓存行:
1public class PaddingExample { 2 private long padding1, padding2, padding3, padding4; // 填充字段 3 private volatile long counter1; 4 private volatile long counter2; 5 private long padding5, padding6, padding7, padding8; // 填充字段 6}
  • 这种方法通过填充额外的字段来确保每个线程独立访问的字段位于不同的缓存行。

3. 字段的重新排列

  • 通过重新排序类中字段的位置,确保访问频繁的字段不共享同一个缓存行。例如,将热字段(即多个线程频繁访问的字段)放在类的前面,而将不常访问的字段放在类的后面。这样可以通过 JVM 的对象布局避免这些字段处于同一个缓存行。
1public class NoFalseSharing { 2 private volatile long counter1; 3 private volatile long counter2; 4 private volatile long counter3; 5 private volatile long counter4; 6}
  • 注意:这种方法需要根据缓存行的大小(通常是 64 字节)来合理安排字段的位置。

4. 使用 java.util.concurrent.atomic

  • 如果可能,使用 java.util.concurrent.atomic 包中的原子变量(如 AtomicLongAtomicInteger 等)。这些原子变量本身是线程安全的,并且在设计时考虑了缓存一致性问题,减少了因伪共享引发的性能问题。
  • 然而,原子操作不一定能完全消除伪共享,因此,如果你在高并发环境下进行大量的原子操作,仍然需要考虑其他优化方法。

5. 合理划分线程工作区

  • 在并行编程中,可以通过合理地划分每个线程的工作区域,确保不同线程之间的工作负载尽量分开,不会互相干扰,减少共享数据的竞争。这在多核处理器上尤其有效。

6. 使用 JVM 参数来优化

  • 一些 JVM 的调优参数,如 -XX:+UseCompressedOops,可能有助于减少内存占用和优化缓存,但并非专门针对伪共享问题。具体的优化需要根据硬件和应用的具体需求来决定。

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