现在的位置: 首页 > 综合 > 正文

什么是伪共享?volatile 是否存在伪共享问题?

2020年02月19日 综合 ⁄ 共 1630字 ⁄ 字号 评论关闭

  LongAdder 工具类。我们知道了 LongAdder 中为了分散热点数据,存在一个 volatile 修饰的 Cell 数组。由于数组的连续存储特性,会存在伪共享问题,你知道 LongAdder 是如何解决的吗?

  在探讨 LongAdder 是如何解决伪共享问题之前,我们要先梳理清一个概念,什么是伪共享和共享 ?

  什么是共享

  共享在 Java 编程里面我们可以这样理解,有一个 Share 类,它有一个 value 的属性。如下:

  public class Share { int value;}

  我们初始化 Share 的一个实例,然后启动多个线程去操作它的 value 属性,此时的 Share 变量被多个线程操作的这种情况我们称之为共享。

  大家都知道在不添加任何互斥措施的情况,多线程操作这个 Share 变量的 value 属性肯定存在线程安全性的问题。那有什么办法可以解决这个问题呢?我们可以使用 volatile 和 CAS 技术来保证共享变量可以安全的被多个线程共享操作使用,不知道 volatile 和 CAS 技术点的同学可以参考往期文章 ReentranLock 实现原理居然是这样?。

  但是由于 volatile 的引入,会带来一些问题。大家都知道 JMM(Java 内存模型)规范了 volatile 具有内存可见性和禁止指令重排序的语义。这俩条语义使得某个线程更新本地缓存中的 value 值后会将其他线程的本地缓存中的 value 值失效,然后其他线程再次读取 value 值的时候需要去主存里面获取 value 值,这样即保证了 value 的内存可见性。

  什么是伪共享

  当然啦,这没有任何问题,但是由于线程本地缓存的操作是以缓存行为单位的,一个缓存行大小通常为 64B(不同型号的电脑缓存行大小会有不同)。因此一个缓存行中不会只单单存储 value 一个变量,可能还会存储其他变量。这样当一个线程更新了 value 之后,如果其他线程本地缓存中同样缓存了 value, value 所在的缓存行就会失效,这意味着该缓存行上的其他变量也会失效,那么线程对这个该缓存行上所有变量的访问都需要从主存中获取。我们都知道 CPU 访问主存的速度相对于访问缓存的速度有着数量级的差距,这就带了很大的性能问题,我们将这个问题称之为 伪共享。

  理解了伪共享到底是什么鬼以后,我们来看看 Java 大师们是怎么解决这个问题的。在早期版本的 JDK 里面你应该见到过类似如下的代码:

  public class Share { volatile int value; long p1, p2, p3, p4, p5, p6;}

  你可能猜到了,定义了几个无用的变量作为填充物,他们会保证一个缓存行里面只保存了 Share 变量,这样更新 Share 变量的时候就不会存在伪共享问题了。但是这种方法存在什么问题呢?

  首先基于每台运行 Java 程序的机器的缓存行大小可能不同,其次由于这些类似填充物的变量并没有被实际使用,可以被 JVM 优化掉,这样就失效了。

  基于此,在 Java 8 的时候,官方给出了解决策略,这就是 Contended 注解。依赖于这个注解,我们在 Java 8 环境下可以这样改善代码:

  public class Share { @Contended volatile int value;}

  使用如上注解,并且在 JVM 启动参数中加入 -XX:-RestrictContended,这样 JVM 在运行时就会自动的为我们的 Share 类添加合适大小的填充物(padding)来解决伪共享问题,而不需要我们手写变量来作为填充物了,这样就更加便捷优雅的解决了伪共享问题。悄悄的告诉你,LongAdder 就是使用 Contended 来解决伪共享问题哒。

  好了,相信你已经了解了什么是伪共享问题,以及早期并发编程大师是如何解决伪共享问题的,最后我们也介绍了在 Java 8 中使用 Contended 来更优雅的解决伪共享问题。Contended 还提供了一个缓存行分组的功能,在上文中我们没有介绍,欢迎有兴趣的小伙伴们自行探索吧。

抱歉!评论已关闭.