伪共享
一、什么是伪共享?
为了了解什么是伪共享,我们需要先知道CPU的缓存。由于计算机系统中CPU和主内存的运行速度差别很大,CPU读写速度很快而主内存很慢。所以CPU和主内存之间通常会有一级或多级高速缓存处理器,即我们通常说的cache缓存。cache一般是集成在CPU内部的,一级缓存内存最小,最靠近CPU,且速度最快。
而cache内部是按行存储的,每一行称为一个cache行。cache行是主内存和CPU进行数据交换的基本单位,大小一般是2n【一般是64字节,部分旧的处理器是32字节】。
当CPU访问某个变量时,会先去cache中找这个变量是否存在,如果存在则直接从缓存中取出,否则去访问主内存,并把主内存中这个变量所在内存区域的一个cache行大小的内存复制到cache中。而此时问题就出现了,因为CPU会把内存块存储到cache中,而内存块中可能不只有一个变量,就会出现问题。核心:缓存行。
考虑这种场景,假设变量long y和long x是在同一个cache行大小的内存块中。此时线程1访问变量long x,会把这个内存块复制到cache中,此时一级缓存中同时有x和y。线程1使用CPU1对变量x进行了修改,由于缓存一致性协议,会导致CPU2中变量x对应的缓存行失效。而此时如果有线程2使用CPU2操作y变量时,由于缓存已失效就必须去二级缓存甚至去主内存中访问y变量。明明是x和y两个不一样的变量,却要因为对x的读写操作频繁去访问主内存。这就是伪共享。
二、为什么会出现伪共享?
是因为多个变量被放到了同一个缓存行中,同时多个线程同时操作同一个缓存行的不同变量。而多个变量会被放到同一个缓存行是因为前面提到过的缓存和内存交换数据的单位就是缓存行。那为什么会这样做呢?是因为在单线程下如果顺序修改一个缓存行的多个变量会因为变量之前已经被加载到缓存里了所以执行速度很快,但如果是多线程并发修改一个缓存行的多个变量就会出现伪共享的问题,从而降低程序运行性能。
比如下面的代码,运行时间就很长,我的电脑会跑41229ms。
public static void main(String[] args) throws InterruptedException { addNum(new Number()); } private static void addNum(Number number) throws InterruptedException { long start = System.currentTimeMillis(); Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000000000; i++){ number.x++; } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000000000; i++){ number.y++; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(System.currentTimeMillis() - start); } public static class Number{ volatile long x; volatile long y; }
三、如何避免伪共享?
这里提供两种方式避免伪共享。
1、创建一个变量时使用填充字段填充该变量所在的缓存行,如上述代码在申请变量时改成如下形式,从结果可以看出只需要14227ms,速度快了很多。
public static class Number{ volatile long x; long p1, p2, p3, p4, p5, p6, p7; volatile long y; }
2、从JDK 8开始可以使用@sun.sc.Contended注解解决伪共享的问题。但是,默认情况下该注解只能用于java核心类(如rt包下的类),如果用户类路径下的类要使用这个注解需要加上JVM参数:-XX:-RestrictContended,默认填充宽度为128,自定义宽度可以设置参数:-XX:ContendedPaddingWidth。