关于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并发编程之美》

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理