乐趣区

关于java:聊聊多线程中的伪共享现象

什么是伪共享?

讲伪共享之前,让咱们先乘坐时光机,回到大学课堂,来重温下计算机组成原理的基础知识。咱们晓得,CPU 和内存的运行速度相差很大,为了解决这个问题,在 CPU 和内存之间会加一级或多级高速缓存(Cache)。这个 Cache 个别是集成在 CPU 外部的,所以也叫 CPU Cache。下图是一个两级 Cache 的 CPU-Cache- 内存架构。

数据在 Cache 中是按行存储的,其中每一行称为一个 Cache 行,如下图所示。它是 CPU 与内存数据交换的根本单位。Cache 行的大小个别为 2 的幂次字节数。

CPU-Cache- 内存架构的工作原理是这样的:当 CPU 拜访某个变量时,首先会从 CPU Cache 里查看是否有该变量,如果有间接获取并返回,否则从内存中获取,而后把该变量所在内存区域的一个 Cache 行大小的内存数据复制到 Cache 中,这就是咱们所说的局部性原理。因为寄存到 Cache 行的不是单个变量,而是一个内存块数据,所以会呈现多个变量放在一个 Cache 行中。当多个线程同时批改同一个缓存行外面的不同变量时,因为同一时刻只容许一个线程操作缓存行,一个线程胜利获取缓存行的修改权时,其余线程会互斥期待,并且因为缓存一致性协定,其余线程相应的缓存行会生效,须要从新从内存获取数据,这无疑消耗了更多工夫。所以相比把每个变量放在不同的缓存行,性能反而有所降落,这就是伪共享景象。
如下图所示,变量 x,y 放在同一个缓存行中,线程 1 操作缓存行的 x 变量,线程 2 操作 y 变量。

如何防止伪共享呢?

在 JDK1.8 之前,通常是通过字节填充的形式。什么意思呢?就是用到一个变量时,补充额定的若干辅助变量,使得这些变量刚好填充斥一个缓存行,这样就防止了多个变量寄存在一个缓存行中。具体看上面示例代码:

static final class PaddedLongField {
  public volatile long value = 0L;
  public long p1,p2,p3,p4,p5,p6;
}

如果 CPU Cache 行大小为 64 字节,那么咱们这里填充了 6 个 long 型的变量,每个 long 型变量占 8 个字节,加上 value 一共 7 *8=56 字节,另外,别忘了 PaddedLongField 是一个类对象,对象头还要占用 8 个字节,所以一个 PaddedLongField 对象占用 64 个字节,刚好填充斥一个缓存行。

在 JDK1.8 之后,提供了一个 @sun.misc.Contended 注解,用来解决伪共享问题。此时,咱们下面的代码就能够简化了:

@sun.misc.Contended
static final class PaddedLongField {public volatile long value = 0L;}

@Contended 注解不仅能够润饰类,也能够润饰变量:

JUC 源码里很多应用这个注解的,比方 Thread 类里 threadLocalRandom 相干的变量:

再比方 LongAdder 外部用到的 Cell 也用了这个注解:

再比方 ForkJoinPool 类下面也润饰了:

最初须要留神下,@Contended 注解默认只用于 Java 外围类,比方 rt.jar 下的类。如果咱们应用程序中想应用这个注解,须要增加一个 JVM 参数:-XX:-RestrictContended,填充的默认宽度为 128 字节,若需自定义宽度则能够用另一个参数:-XX:ContendedPaddingWidth=xxx

本文就到这里啦,若这篇文章对你有所帮忙的话,点个赞再走叭!谢谢反对!

参考资料:
《Java 并发编程之美》

退出移动版