背景
上文:内存屏障是什么? 本文是继上文的解决内存不一致的另一种实现方式。根据上文我们了解到cpu加载执行计算的流程:
1、程序和数据被加载到主内存中(Main Memory)
2、指令和数据被加载到CPU的高速缓存(L1、L2、L3)
3、CPU执行完指令,结果同步到高速缓存(L1、L2、L3)
4、最后将高速缓存中的数据同步到主内存中(Main Memory)
所以在多CPU的情况下加载回主内存或缓存需要保持缓存一致性要么加锁,要么用于本文的MESI来实现。
缓存一致性协议-MESI是什么?
MESI是Modify(修改)、Exclusive(独享、互斥)、Shared(共享)、Invalid(无效)首字母组成的。用于对单个缓存行的数据进行加锁,不会影响到内存中其他数据的读写。
状态 | 描述 | 监听任务 |
---|---|---|
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |
缓存行是什么?
缓存行(cache line)是缓存存储数据的单位,也是cpu缓存的最小单位。一般大小为64Byte。所以上面的四种状态就是保存在缓存行中,每个状态需要2个Bit位去存储(就是Flag)。、
什么是缓存行伪共享?
当多个cpu在多线程修改互相独立的变量时,如果其中某些变量共享同一个缓存行,就会无意中影响彼此的性能,这种称为伪共享。
个人理解:就像你用有道笔记一样,在苹果电话和windows有同一个文件,当一方修改会同步给另一方,导致另一方被更新了。这种有点类似内存的可见性,比如某些变量用volotail。
如何避免伪共享?
方法1:利用jdk8的注解:@sun.misc.Contended ,通过添加该注解会自动补齐缓存行。
代码语言:javascript复制@sun.misc.Contended
public final static class TestlONG {
public volatile long test = 0L;
}
注意该注解需要开启jvm配置:
代码语言:javascript复制-XX:-RestrictContended
方法2 :在两个long类型的变量之间再加7个Long类型。(一个缓存行是64个字段,一个Long类型是8字节)
代码语言:javascript复制class Test {
volatile long a;
long p1, p2, p3, p4, p5, p6, p7;
volatile long b;
}
MESI状态有哪些?
以上是触发的MESI状态图,分别为本地读取、本地写入、远端读取、远端写入,如下表描述。
触发事件 | 描述 |
---|---|
本地读取(Local read) | 本地cache读取本地cache数据 |
本地写入(Local write) | 本地cache写入本地cache数据 |
远端读取(Remote read) | 其他cpu内核读取了DRAM中当前内核的缓存行 |
远端写入(Remote write) | 其他cpu内核写入了DRAM中当前内核的缓存行 |
不同场景的触发如下
假设我们有两个核core0和core1当发生不同状态修改的时候会发生不同的事件。
状态是M(修改)
事件 | 行为 | 下一个状态 |
---|---|---|
local read | 直接从core0的cache中读,状态不发生改变 | M |
local write | 直接修改core0的cache数据,状态不变 | M |
remote read | core1需要最新数据,将core0的内容写回到RAM中core1从RAM再读取值,core0和core1的缓存行标志为设置为S | S |
remote write | 先将core0的值写回到RAM中core1读取值并修改,core0的状态变为I,core1的状态变为M | I |
状态是S(共享)
事件 | 行为 | 下一个状态 |
---|---|---|
local read | 直接从core0的cache中读,状态不发生改变 | S |
local write | core0直接修改cache,状态变为M。core1变为I(失效) | M |
remote read | core1读的和core0的一样的数据,状态不变 | S |
remote write | core1对应成了上面core0的local write,core1的cache变为M,core0的变为I(失效) | I |
状态是E(独占)
事件 | 行为 | 下一个状态 |
---|---|---|
local read | 直接从core0的cache中读,状态不发生改变 | E |
local write | 直接修改core0的cache,状态变为M | M |
remote read | core1发送读事件,core0和core1需要共享内容,所以状态都变为S | S |
remote write | core0将内容置为I(失效) | I |
状态是I(失效)
事件 | 行为 | 下一个状态 |
---|---|---|
local read | 如果core1没有内容,则core0读取内容,状态为E如果core1有内容,状态为M,则core1需要写回到RAM,然后core0读取,状态变为S如果core1有内容,状态为S或者E,那么core0直接读,core0和core1都变为S | E or S |
local write | core0需要从RAM拉取数据如果core1没有内容,那么core0就直接拉取,修改后设置为M如果core1有内容,状态为M,那么core1需要先写回RAM,然后core0读取最新到cache,修改并设置为M,core1变为I如果core1有内容,状态为S或者E,那么core0读取并设置为M,core1为I(失效) | M |
remote read | 已经失效,只和自己读写有关,和其他的读写无关,状态不变。 | I |
remote write | 已经失效,只和自己读写有关,和其他的读写无关,状态不变。 | I |
最终汇致如下图
最后
关于MESI的确是一种解决内存屏障一种方式,并且被各大语言规范引用了,当然该内容还是比较底层涉及硬件知识及相关的硬件处理配置,所以可能看起来比较深奥,建议新手同学可以看看下面的参考资料,深入学习了解。
参考资料:
https://baike.baidu.com/item/MESI协议/22742331
https://note.youdao.com/ynoteshare/index.html?id=73fc01483ff8b40c47d6898ad17a66c8&type=note&_time=1668342395100?auto
https://juejin.cn/post/7065616174712619015
https://www.cnblogs.com/yanlong300/p/8986041.html
https://www.cnblogs.com/cyfonly/p/5800758.html 写得好~
https://juejin.cn/post/7001122798122827806
https://www.zhihu.com/question/296949412#!
https://zhuanlan.zhihu.com/p/351550104
https://www.cnblogs.com/gunduzi/p/13590528.html
视频资料:
https://www.bilibili.com/video/BV1fK4y1E7NC/?vd_source=7d0e42b081e08cb3cefaea55cc1fa8b7
https://www.bilibili.com/video/BV1pC4y1W7zS/?vd_source=7d0e42b081e08cb3cefaea55cc1fa8b7