面时莫慌 | 你好,请谈谈volatile关键字?(三)


theme: channing-cyan

这是我参与更文挑战的第26天,活动详情查看: 更文挑战
紧接着上一篇你好,请谈谈volatile关键字?(二)

4.4 缓存一致性协议MESI

现在来正式谈谈MESI,上面说MESI协议具有四个状态,这四种状态指的是4个单词的首字母,具体包括ModifiedExclusiveSharedInvalid,用2个bit表示,几种状态解释如下。

  • Modified 表示Cache Line有效,数据只存在当前Cache中,并且数据是已经被修改了,与主存中的是不一致的。
  • Exclusive 表示Cache Line有效,数据只存在当前的Cache中,数据和主存保持一致的。
  • Shared 表示Cache Line有效,数据并不只是存在当前的Cache中,被多个Cache共享,各个Cache 与主存数据都一致。
  • Invalid 表示当前缓存行已经失效。

MESI协议中,每个高速缓存的控制器不单单知道自己的读写操作,而且还监听其它高速缓存的读写操作,这个就是嗅探技术,控制器针对当前Cache Line处于的状态进行不同的监听任务。

4.4.1 状态变化流程

到底是如何监听任务的,我们通过一个简单的例子来分析一下。假设我们现在有一个双核的CPU,主存里面存储着一个i的变量,值为1,现在CPU要做一些运算操作,需要将i读取到缓存中。

image.png

步骤1:图上CPU1从主存中读取数据到缓存,当前缓存的存储的变量i=1,缓存行的状态时E,也就是独占,时刻监听着有没有其它缓存也要从主存中加载该变量。

image.png

步骤2:图上CPU2也试图从主存中读取变量i,加载到缓存中,CPU1监听到这个事件,于是CPU1立刻做出变化,更改状态为SCPU2也同时读取到数据,状态也为S。此时两个CPU Cache Line 存储的变量i=1,都在监听有没有事件要使缓存自己置为I无效态,或者其它缓存要独享变量的请求。

image.png

步骤3:图上CPU1计算完成后,需要修改变量i=2,缓存管理器先设置Cache Line 的状态为M修改态,然后发起事件通知其它CPUCPU2收到事件通知,设置Cache Line的状态为I无效态。CPU1 监听着其它缓存要读取主内存的事件。CPU2的缓存行因为状态时无效的,所以缓存行失效。

image.png

步骤4:图上,CPU2运算要用到变量i,因为存储i的缓存行失效,去主动同步主内存。CPU1收到有其它CPU要读取主存的请求,赶在读取之前,先把修改后的变量同步到主存,同步完以后,主存上的变量i=2,然后CPU1缓存管理器设置缓存行的状态为E。然后按照步骤4,两个CPUCache Line最后状态都变为S

image.png

4.4.2 状态变化原则

总的来说,对于CPU读写操作缓存行,MESI协议遵循以下的原则:

  • CPU读请求:缓存行当前状态处于M E S状态都可以被读取,处于I状态下,CPU只能从主存中读取数据。
  • CPU写请求:缓存行当前状态处于M E 状态才可以被直接写,处于I状态下,缓存行已经失效,无法进行读取操作;处于S状态,能写的前提条件是将其它缓存行设置为无效。
4.4.3 MESI 带来的问题

虽然通过MESI协议的四种状态和嗅探技术,实现了缓存的一致性,但也带来一些问题。

上面我们谈到,如果CPU要将计算后的结果写入Cache Line 中,需要发送一个失效的通知给其它存储了相同数据的CPU,并且必须等到他们的状态变更完成后才能进行相应的写入操作,在整个期间,该CPU在同步地阻塞的等待,十分影响CPU的性能。

为了解决阻塞等待的问题,在CPU中又引入了Store Buffer,通过这个buffer,CPU要修改缓存中的值时,只需要将数据写入这个buffer,就可以去执行其它指令了。然后当收到其它CPU修改指定缓存行的状态为I无效态以后,再将buffer的数据存储到Cache Line,然后必要时,再同步到主存中。

这种方案是异步的,解决了CPU同步等待阻塞的问题。但同时也引入了新的问题。

  • 因为是一个异步操作,具体什么时候收到其它CPU状态变更的通知是不明确的,所以导致Store Buffer的数据什么时候写入Cache Line也是不确定。
  • 当未收到其它CPU状态变更之前CPU有可能会来读取数据,首先会从Store Buffer中读,如果没有,再读Cache Line,如果还没有,再读主存。

新的问题,带来的巨大的影响就是指令重排序

我们通过一个例子分析具体是什么问题。

``` int value =1; bool finish = false;

void runOnCPU1(){ value = 2; finish = true; }

void runOnCPU2(){ if(finish){ assert value == 2; } } ```

我们假设#runOnCPU1#runOnCPU2 两个方法分别运行在两个独立的CPU上。 我们很容易想到肯定不会有断言执行。当事实真的如此吗,以下是一种可能的场景。

CPU1 缓存行上缓存了两个关键变量,状态如下:

| | value | finish | | --------------- | ----- | ------ | | CacheLine状态 | S | E |

CPU1在执行#runOnCPU1方法时,会先把value=2写入到Store Buffer中,继续执行finish=true这条指令,与此同时,也通知了其它存储相同变量的CPU 设置缓存行的状态为I无效态,并异步的等待执行结果回执。

因为当前存储finish变量的Cache Line的状态为E独占,所以无需通知其它CPU,立刻就能将finish=true写入Cache Line。这个时候CPU2开始执行#runOnCPU2方法,会从主存中读取finish,按照文章上面介绍的状态变化步骤,会轻松读到finish=true,此时两个CPU存储finishCache Line状态都为S,并且主存的finish=trueCPU2继续执行assert value == 2;这条指令,首先要去从主存中获取value的值,因为CPU1修改value的值还放在Store Buffer,所以CPU2取到的值会是1。

也就是说,我们能看到的现象是,在方法#runOnCPU1中,finish赋值早于value的赋值,跟我们预期有差异,这个就是指令重排序带来的可见性问题

这种可见性问题,可以基于JMM(Java 内存模型)内存屏障去解决,恰恰好,这个就是volatile保证多线程环境下可见性的杀手锏。
篇幅较长,继续阅读请点击【面时莫慌】你好,请谈谈volatile关键字?(四)


哥佬倌,莫慌到走!觉好留个赞,探讨上评论。欢迎关注面试专栏面时莫慌 | Java并发编程,面试加薪不用愁。也欢迎关注我,一定做一个长更的好男人。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值