CPU缓存一致性协议MESI
1、MESI
1.1、什么是缓存一致性协议MESI
现在的计算机一般都是多CPU多核的,所以就会出现多个CPU或者多个核在同一时间访问同一个变量的情况,缓存一致性协议就是来解决当多个核同时对一个变量进行操作的时候,保证数据一致性的问题。
1.2、MESI的缓存行
说到缓存,缓存的最小概念是缓存行,64位操作系统的缓存行一般占64byte,MESI缓存一致性就是根据缓存行来的,如果一个对象的占用空间大于64字节,那么就不能保证它的原子性了,这个时候就得从缓存一致性升级到总线锁了。
MESI也有自己的缓存行,每个变量在缓存行中有四种状态分别是E(exclusive)、M(modified)、S(shared)、I(invalid),如下图所示:
M:代表缓存行里面的数据已经被修改了,后续会写到内存中。
E:代表该数据只被该CPU或者该核缓存,不存在多个CPU或者多个核同时操作的情况。
S:代表该数据被多个CPU或者多个核缓存,存在多个CPU或者多个核同时操作的情况。
I:代表该缓存行里面的数据已经失效了,也就是被其它CPU或者核已经修改掉了。
1.3、volatile对MESI的应用
当我们对一个变量加上 volatile 关键字时,底层硬件会对这个变量加上一个lock前缀,在 Pentium 和早期的 IA-32 处理器中,LOCK 前缀会使处理器执行当前指令时产生 一个 LOCK#信号,这总是引起显式总线锁定出现,而这个总线锁定就会触发 MESI 协议,所以,volatile就可以保证数据的可见性。
2、MESI优化和他们引入的问题
缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的 阻塞都会导致各种各样的性能问题和稳定性问题。
解决方案如下:
当我们CPU修改了一个变量之后,会将它放进一个叫Store Bufferes(存储缓存)中,然后再去通知其它的用到了该变量的CPU,其它用到了该变量的CPU收到通知后,会将自己的备份数据放到一个特殊队列中(失效队列),然后立刻发送给修改该变量的CPU一个通知,修改变量的CPU在收到通知之后,会在合适的时间将数据从Store Bufferes 中写到主存中去,其它的CPU先从 Store Bufferes 里面获取数据,等到合适的时间,再将失效队列的数据丢弃掉。
10、辅助知识
10.1、当加上CPU时Java代码的执行过程
10.2、总线锁
所谓总线锁就是在总线上加锁,早期技术还不是那么发达的时候,那时候还没有三级缓存,所以就出现了CPU寄存器直接访问主存的情况,那么就可能会出现两个CPU同时访问一个变量的情况,如果两个CPU同时对这个变量进行写的操作,那么肯定就会出现问题,所以这个时候就出现了总线锁,同一时间的时候只能有一个CPU去访问这个变量,那么问题就来了,无论我们有多少CPU,有多少个核,都只能发挥单CPU单核的性能,所以这个时候就出现了类似于mesi的缓存一致性解决方案。
10.3、缓存行伪共享
10.3.1、什么是伪共享?
CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变 量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。
下面举个栗子:
现在有2个long 型变量 a 、b,如果有t1在访问a,t2在访问b,而a与b刚好在同一个 cache line中,此时t1先修改a,将导致b被刷新!
10.3.2、怎么解决伪共享?
Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:- RestrictContended 才会生效,这样的话我们的一个变量就占一个缓存行了。