一、相关概念
(1)CPU缓存介绍:
以近代CPU的视角来说,它们的作用都是作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3再次。
(2) 任务管理器可以看到其中的大小
(3)L1 L2 L3详细介绍:
L1(1级)高速缓存是计算机系统中存在的最快内存。在访问优先级方面,L1缓存具有CPU在完成特定任务时最可能需要的数据。L1缓存通常也有两种分割方式,分为指令缓存和数据缓存。指令高速缓存处理有关CPU必须执行的操作的信息,而数据高速缓存保存要在其上执行操作的数据。
L2(级别2)缓存比L1缓存慢,在大多数现代CPU中,L1和L2高速缓存存在于CPU内核本身,每个内核都有自己的高速缓存。
L3(Level 3)缓存是最大的缓存单元,也是最慢的缓存单元。它的范围在4MB到50MB之间。现代CPU在CPU裸片上 有专用空间用于L3缓存,占用了大量空间
(4) 缓存命中或错过和延迟
数据从RAM流到L3缓存,然后是L2,最后是L1
当处理器正在寻找执行操作的数据时,它首先尝试在L1高速缓存中找到它。如果CPU能够找到它,则该条件称为缓存命中。
然后它继续在L2中找到它,然后在L3中找到它。
如果找不到数据,它会尝试从主存储器访问它。这称为缓存未命中。
(4)高性能异步处理框架 Disruptor,它被誉为“最快的消息框架”,其 LMAX 架构能够在一个线程里每秒处理 6百万 订单!在讲到 Disruptor 为什么这么快时,接触到了一个概念———— 伪共享( false sharing )
其中提到:缓存行上的写竞争是运行在 SMP(多对称系统) 系统中并行线程实现可伸缩性最重要的限制因素。由于从代码中很难看出是否会出现伪共享,有人将其描述成无声的性能杀手。
伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
缓存行:缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。
我们可以通过缓存行填充来消除伪共享,上图是两个long类型的x变量和y变量。他们是连续存储的,变量x,y同时被放到了CPU的一级和二级缓存,当线程1使用CPU1对变量x进行更新时候,
首先会修改cpu1的一级缓存变量x所在缓存行,这时候缓存一致性协议会导致cpu2中变量x对应的缓存行失效,那么线程2写入变量x的时候就只能去二级缓存去查找,这就破坏了一级缓存,而一级缓存比二级缓存更快。
更坏的情况下如果cpu只有一级缓存,那么会导致频繁的直接访问主内存。
二、为什么会产生伪共享
伪共享的产生是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同变量。那么为何多个变量会被放入一个缓存行那。其实是因为Cache与内存交换数据的单位就是Cache,当CPU要访问的
变量没有在Cache命中时候,根据程序运行的局部性原理会把该变量在内存中大小为Cache行的内存放如缓存行。
long a; long b; long c; long d;声明了四个long变量,假设cache行的大小为32个字节,那么当cpu访问变量a时候发现该变量没有在cache命中,那么就会去主内存把变量a以及内存地址附近的b,c,d放入缓存行。
也就是地址连续的多个变量才有可能会被放到一个缓存行中,当创建数组时候,数组里面的多个元素就会被放入到同一个缓存行。
产生原因:
数据X、Y、Z被加载到同一Cache Line中,
线程A在Core1修改X,线程B在Core2上修改Y
根据MESI,假设是Core1是第一个发起操作的CPU核,Core1上的L1 Cache Line由S(共享)状态变成M(修改,脏数据)状态,然后告知其他的CPU核,图例则是Core2,引用同一地址的Cache Line已经无效了;
当Core2发起写操作时,首先导致Core1将X写回主存,Cache Line状态由M变为I(无效),而后才是Core2从主存重新读取该地址内容,Cache Line状态由I变成E(独占),最后进行修改Y操作, Cache Line从E变成M。可见多个线程操作在同一Cache Line上的不同数据,相互竞争同一Cache Line,导致线程彼此牵制影响,变成了串行程序,降低了并发性。
解决方法:
此时我们则需要将共享在多线程间的数据进行隔离,使他们不在同一个Cache Line上,从而提升多线程的性能。即 缓存行的填充
附图:MESI协议
三、如何消伪共享
(1)JDK8之前一般都是通过字节填充的方式来避免,也就是创建一个变量的时候使用填充字段填充该变量所在的缓存行,这样就避免了多个变量存在同一个缓存行。
public final static class FilledLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
}
假如Cache行为64个字节,那么我们在FilledLong类里面填充了6个long类型变量,每个long类型占用8个字节,加上value变量的8个字节总共56个字节,另外这里FilledLong是一个类对象,
而类对象的字节码的对象头占用了8个字节,所以当new一个FilledLong对象时候实际会占用64个字节的内存,这个正好可以放入Cache的一个行。
(2)在JDK8中提供了一个sun.misc.Contended注解,用来解决伪共享问题,上面代码可以修改为如下:
@sun.misc.Contended
public final static class FilledLong {
public volatile long value = 0L;
}
(3)上面是修饰类的,当然也可以修饰变量,比如Thread类中的使用:
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;