什么是伪共享
为了解决计算机系统中主内存与 CPU 之间运行速度差问题,会在 CPU 与主内存之间添加一级或者多级高速缓冲存储器( Cache )。这个 Cache 一般是被集成到 CPU 内部的, 所以也叫 CPU Cache ,如下图所示是两级 Cache 结构。
image.png在 Cache 内部是按行存储的,其中每一行称为一个 Cache 行。Cache 行是 Cache 与主内存进行数据交换的单位,Cache 行的大小一般为 2 的幂次数字节。
image.png当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个 Cache 行大小的内存复制到 Cache 中。由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到 一个 Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享,如下图:
image.png在上图中,变量 x 和 y 同时被放到了 CPU 的一级和二级缓存,当线程 1 使用 CPU1 对变量 x 进行更新时,首先会修改 CPU1 的一级缓存变量 x 所在的缓存行,这时候在缓存一致性协议下,CPU2 中变量 x 对应的缓存行失效。那么线程 2 在写入变量 x 时就只能去二级缓存里查找,这就破坏了一级缓存。而一级缓存比二级缓存更快,这也说明了多个线程不可能同时去修改自己所使用的 CPU 中相同缓存行里面的变量。更坏的情况是,如果 CPU 只有一级缓存,则会导致频繁地访问主内存。
为何会出现伪共享
伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。那么为何多个变量会被放入一个缓存行呢?其实是因为缓存与内存交换数据的单位就是缓存行,当 CPU 要访问的变量没有在缓存中找到时,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。
long a;
long b;
long c;
long d;
如上代码声明了四个 long 变量,假设缓存行的大小为 32 字节,那么当 CPU 访问变量 a 时,发现该变量没有在缓存中,就会去主内存把变量 a 以及内存地址附近的 b、c、d 放入缓存行。也就是地址连续的多个变量才有可能会被放到一个缓存行中。当创建数组时,数组里面的多个元素就会被放入同一个缓存行。那么在单线程下多个变量被放入同一个缓存行对性能有影响吗?其实在正常情况下单线程访问时将数组元素放入一个或者多个缓存行对代码执行是有利的,因为数据都在缓存中,代码执行会更快。
如何避免伪共享
在 JDK 8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同 一个缓存行中,例如如下代码。
public final static class FilledLong {
public volatile long value = OL;
public long pl,p2,p3,p4,p5,p6;
}
假如缓存行为 64 字节,那么我们在 FilledLong 类里面填充了 6 个 long 类型的变量,每个 long 类型变量占用 8 字节,加 上 value 变量的 8 字节总共 56 字节。另外,这里 FilledLong 是一个类对象,而类对象的字节码的对象头占用 8 字节,所以一个 FilledLong 对象实际会占用 64 字节的内存,这正好可以放入一个缓存行。
JDK 8 提供了 一个 sun.misc.Contended 注解,用来解决伪共享问题。将上面代码修改为如下。
@sun.misc.Contended
public final static class FilledLong {
public volatile long value = OL;
}
次注解除了可以修饰类,也可以修饰变量,比如在 Thread 类中。
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
Thread 类里面这三个变量默认被初始化为 0 ,这三个变量会在ThreadLocalRandom 类中使用。
需要注意的是,在默认情况下,@Contended 注解只用于 Java 核心类,比如此包下的类。如果用户类路径下的类需要使用这个注解,则需要添加 JVM 参数:-XX:-RestrictContended。填充的宽度默认为 128 ,要自定义宽度则可以设置 -XX:ContendedPaddingWidth 参数。
网友评论