1、什么是伪共享?
1.1背景
1.1.1 CPU缓存架构
我们知道CPU的处理速度与内存、硬盘的访问速度有很到的关系,为了缓解CPU处理速度与内存、硬盘的访问速度的差别,在当代的CPU中,普通引入了CPU缓存,那CPU缓存的架构是怎样的,直接上图:
当代CPU架构基本都是三层架构,这是基于访问速度和容量成本做出的权衡,从图中可以看出:
从L1到L3到RAM,访问速度相对变慢,存储变大
L1、L2基本都是被单核独享,L3被插槽上的CPU的所有核共享
当CPU运算需要数据时,先从L1上要,L1上没有就到L2上要,L2上没有就到L3上要,直到Ram,硬盘,越远就越耗时:
1.1.2缓存行cache line
CPU缓存是有缓存行组成的,一个缓存行一般是64个字节,CPU读取数据是以缓存行为单位的读取,这意味着即使是读1个字节的数据,CPU也要读取这个数据所在的连续的64个字节的数据,如果使用的数据结构中的数据项不是彼此相邻连续的,如链表,那么读数据的时候就得不到免费缓存带来的好处,在java中,数组中的数据通常是连续的(数组的连续存储不是jvm规范中的要求,在某些jvm中,大中型数据项不是分配在连续的空间),所以数组的访问速度比链表要快。
1.1.3 缓存失效
在java中,long类型占8个字节,这就意味着,当读一个long类型的变量,也会读取其相邻的7个long类型变量(不是long类型的变量按占用的字节数计算个数,如int类型的变量占4个字节,那么就64-8个字节,就可以存储14个int类型的变量),在基于mesi协议下,其它的线程此时再读取其中的一个long类型变量,那么这个long类型变量所在的缓存行就会失效,需要重新读取缓存,这就是缓存失效。
1.2 伪共享
CPU在读取数据时,是以一个缓存行为单位读取的,假设这个缓存行中有两个long类型的变量a、b,当一个线程A读取a,并修改a,线程A在未写回缓存之前,另一个线程B读取了b,读取的这个b所在的缓存是无效的(前面说的缓存失效),本来是为了提高性能是使用的缓存,现在为了提高命中率,反而被拖慢了,这就是传说中的伪共享。
1.3 那么如何消除伪共享呢?
当多线程修改相互独立的变量时,如果这些变量在同一个缓存行,就会无意中影响彼此的性能,这个就是伪共享,那么我们怎么消除呢?
我们先看一个因为有伪共享而影响性能的例子:
/**
* 伪共享演示
*/
public class FalseSharingDemo {
public static void main(String[] args)throws InterruptedException {
testPointer(new Pointer());
}
private static void testPointer(Pointer pointer)throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 =new Thread(() -> {
for (int i =0; i <100000000; i++) {
pointer.a++;
}
}, "A");
Thread t2 =new Thread(() -> {
for (int i =0; i <100000000; i++) {
pointer.b++;
}
}, "B");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis() - start);
System.out.println(pointer.a +"@" + Thread.currentThread().getName());
System.out.println(pointer.b +"@" + Thread.currentThread().getName());
}
}
class Pointer {
//在一个缓存行中,先会存储a
volatile long a;
// 放开下面这行,解决伪共享的问题,提供了性能 --- 方法1
long p1, p2, p3, p4, p5, p6, p7;
volatile long b;
}
程序输出:
3370 //基本在3000ms多的耗时
100000000@main
100000000@main
1.4解决伪共享的方法
知道了伪共享产生的原理,就不难找到解决伪共享的方法了,总结起来大概有下面三个
1.4.1 在变量的后背凑齐64个字节的变量
如上面的pointer类改成:
classPointer {
//在一个缓存行中,先会存储a
volatile long a; //需要volatile,保证线程间可见并避免重排序// 放开下面这行,解决伪共享的问题,提高了性能
long p1, p2, p3, p4, p5, p6, p7;
volatile long b; //需要volatile,保证线程间可见并避免重排序
}
再运行上面的程序,输出:
1284 //基本在1000ms多的耗时,性能提高了2倍
100000000@main
100000000@main
1.4.2 使用消除了伪共享结构的类
如上面的程序不直接使用long类型,我们自动以一个long类型:MyLong
classPointer2{
MyLong
a = newMyLong();
MyLong b = newMyLong();
}
classMyLong {
volatile longvalue;
longp1,p2,p3,p4,p5,p6,p7;
}
让后再简单的改下:
private static void testPointer(Pointer2 pointer)
pointer.a++ 改成 pointer.a.value++
pointer.b++ 改成 pointer.b.value++
再次执行程序,输出:
1285 //与不消除伪共享前,性能提高2倍
com.javastack.mtc.cacheline.MyLong@1de0aca6@main
com.javastack.mtc.cacheline.MyLong@255316f2@main
1.4.3 使用jdk8注解:@sun.misc.Contended
修改 MyLong 如下:
@sun.misc.Contended
classMyLong {
volatile longvalue;
}
或者:
classPointer {
volatile long a;
@Contended
volatile long b;
}
@Contended注解使用方法
@Contended 注解会增加目标实例大小,要谨慎使用。默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,需要在jvm启动参数上设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。参考《Java性能权威指南》。
1.3 使用了伪共享的大牛例子?
1.3.1 jdk中的ConcurrentHashMap类
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
1.3.2 jdk8中LongAdder的父类Striped64
// Striped64中的内部类Cell,使用@sun.misc.Contended注解,说明里面的值消除了伪共享
1.3.2 著名的disruptor中的ringBuffer
1.5 小结
CPU具有多级缓存,越接近CPU的缓存越小也越快;
CPU缓存中的数据是以缓存行为单位处理的;
CPU缓存行能带来免费加载数据的好处,所以处理数组性能非常高;
CPU缓存行也带来了弊端,多线程处理不相干的变量时会相互影响,也就是伪共享;
避免伪共享的主要思路就是让不相干的变量不要出现在同一个缓存行中;
一是每两个变量之间加七个 long 类型;
二是创建自己的 long 类型,而不是用原生的;
三是使用 java8 提供的注解;
更多干货在公号【java栈长】