伪共享

在之前volatile的文章中介绍过CPU缓存,它是用于解决计算机中主内存和CPU之间运行速度差的问题。在CPU缓存内部是按行存储的,所以每一行也被称作缓存行。缓存行是与内存进行数据交换的单位,大小一般是2的幂次数字节。

当CPU访问某个变量时,会先从缓存中读取,若没有就去内存中读取,然后将变量所在的内存区域中的一个缓存行大小的内存复制到缓存中。至于为什么不是复制单个变量而是复制变量所在的内存块呢,这是因为程序局部性原理。

程序局部性原理

是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分,相应地,执行所访问的存储空间也局限于某个内存区域。

局部性原理又表现为:时间局部性和空间局部性。

  • 时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
  • 空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。

在上述CPU缓存场景,体现了空间的局部性,对于单线程环境下,假设访问了该变量后,不久访问附近的变量,因为提前就将它们加载进了缓存,会提高很多性能。但实际上并发环境下,多个线程同时修改一个缓存行内的多个变量时,由于同时只能由一个线程去读写缓存行,相对于每个变量都放到一个缓存行,性能会下降,这就是伪共享

 

避免伪共享

伪共享是因为程序局部性原理导致的,所以只要在创建变量的时候填充无关的字节到同一个缓存行中,这样就相当于每个变量都放到一个缓存行中,从而提升了性能,如下代码:

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

假如缓存行有64字节,在上面的demo 中填充了6个long类型的变量,每个long类型变量占用8个字节,加上value变量一共56个字节,而且demo类是一个类对象,它的对象头占8个字节,所以一个FillCacheDemo实际占用了64字节正好是一个缓存行。

目前JDK8,提供了注解来解决伪共享的问题@sun.misc.Contended

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

默认@Contented注解只用于java核心类,比如rt包下的类。如果用户类需要用这个注解,则需要添加JVM参数:-XX:-RestrictContended。默认填充宽度是128,要自定义宽度可以设置-XX:-ContendedPaddingWidth参数。

 

参考:《JAVA并发编程之美》

posted @ 2019-07-16 18:24  morphの  阅读(159)  评论(0编辑  收藏  举报