Java基础: CPU缓存行、无效化队列

一. 什么是CPU缓存?

答: CPU缓存是CPU与内存之间的一块临时数据交换器,用于解决CPU运算速度与内存读写速度不匹配的矛盾。

二. 什么是CUP的三级缓存?

CPU的缓存被分成了三个级别: L1,L2,L3,其中,越接近CPU的缓存容量越小,但运行速度越接近CPU。
在这里插入图片描述
每个核心都有2个L1缓存,1个L2缓存,同一个CPU插槽之间的核共享一个L3缓存。
我觉得没必要搞懂L1、L2、L3各级是怎样交互的,只要知道有缓存,分成三个级别,CPU与缓存交换数据,缓存和内存交换数据,就够了。那么为什么要搞三层出来呢?大概就是因为随时技术的发展,CPU缓存与内存的速度差距越拉越大,原本只做了L1,结果不够用了,所以做了L2。做出L3是为了让多个核心之间能够互相通信。

三. CPU核心、缓存行、消息总线、内存是如何交互的呢?

  • CPU从内存中读取数据
    首先程序读取数据到主内存,接着读取到CPU的高速缓存行L3,再逐层(L3->L2->L1)读取,最后CPU核心读取L1缓存行,并获得数据。
  • CPU计算数据并写回内存
    CPU计算数据后,会把数据写入高速缓存层,接着逐层传递,最后由L3把数据写入主存。

L3 高速缓存行充当了消息总线的角色,看上去就是用于CPU与CPU之间交互、通信的这么一条信道吧。在同一个CPU插槽之间的核共享一个 L3 缓存。

四. 多核CPU更新数据时面临的脏数据问题

假设现在有两个CPU,分别是CPU1和CPU2。按照步骤,执行了以下操作:

  1. CPU1从内存中读取了一条数据,从刚才的三级缓存图中很容易知道,既然CPU核心都能读到数据,那么CPU的高速缓存一定已经读到,并且缓存了这条数据。
  2. CPU2重复上述操作,所以CPU2的高速缓存中也一定能缓存这条数据。
  3. CPU1修改了这条数据,并且放回了自己的高速缓存行,但是并没有把数据写入主内存。
  4. CPU2再次读取数据时,发现自己的高速缓存行中已经有这条数据,所以CPU2就直接读取了,不会再费力气去找主内存找这条数据。

其实就算CPU2去找主内存查这条数据,查到的也是脏数据,因为CPU1只是把新数据写到了自己的高速缓冲区,没有写入内存。从上一张图中轻松得知,每个CPU都分别有着自己那一套CPU缓存,别人的缓存和自己没半毛钱关系。

但是现在CPU2读取到的是脏数据啊,有没有办法解决呢?

五. 解决多核CPU写操作面临的脏数据问题(MESI)

CPU的缓存是由若干个缓存行组成的,缓存行的大小是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。缓存行也有地址,对于不同的CPU核心内地址一致的缓存行来说,它们对应主内存的地址也是一样的,只要保证它们的一致性,就能保证数据在多个缓存之间的一致性。

从第四章我们知道,多核CPU在更新数据时,可能会出现不同核心之间,相同缓存行数据不一致的问题。为了较好的解决这个问题,业界发明了一套MESI协议。

5.1 MESI协议的状态

  • M(修改 Modified)
    该缓存行有效,缓存的数据被修改了,与内存(相同的内存地址上存放)的数据不一致,数据只存在于当前的缓存中。
  • E(独享、互斥Exclusive)
    该缓存行有效,缓存的数据与内存(相同的内存地址上存放)的数据完全一致,数据存在于当前的缓存和内存中。至于别的核心的缓存拿不到这个数据。
  • S(共享 Shard)
    该缓存行有效,缓存的数据与内存(相同的内存地址上存放)的数据完全一致,数据存在于当前的缓存、其它核心的缓存以及内存中。
  • I(无效 Invalid)
    该缓存行无效。

如果变量是共享变量,那么当某个CPU核心在修改这个变量时,就会发出信号告知其它的CPU核心,将这个变量置为失效,其它CPU再使用这个共享变量时就会从内存中读取。具体的步骤如下:

  1. 某个CPU核心修改一个共享变量(Shard),由于是共享变量,说明可能还有其它的CPU核心在使用,所以当前CPU核心就会向Bus总线发送一条消息,消息的内容就是: “xxx地址段内的数据发生了变化”。
  2. 其它的CPU核心都会监听到Bus总线上的消息(嗅探机制),并把自己的高速缓冲区(L1)中对应地址段的缓存行的状态置为失效(Invalid)。
  3. 其它的CPU缓存向Bus总线发送Invalid ack,表示自己已经完成无效化的操作了。
  4. 当收到了所有CPU核心的ack后,发起修改操作的CPU核心会把共享变量对应缓存行的状态由"Shard"置为"Exclusive",然后再修改缓存行内的数据,接着修改缓存行的状态为"Modified",最后把缓存行(内的数据)写回到主存(对应的地址段)中。
  5. 此时,其它的CPU核心再次获取这个内存地址的数据时,就会发现,自己的高速缓冲区中不存在这条数据(状态被置为无效了),所以强迫自己去主存中获取获取最新的数据。

这里有一个非常大的问题,就是说,CPU需要在等待所有的Invalid ack之后才会进行后面的操作,这会让当前的CPU产生阻塞,浪费性能,于是就出现了写缓冲器和无效队列。

在学习上述步骤时,我想到了一种比较极端的场景。

比如现在有线程1和线程2,执行它们的分别是cpu-01和cpu-02,,并发的操作一个共享变量flag。线程1打算修改flag的值,所以它会修改执行自己的CPU对应的高速缓存行中的值,并把数据写回到内存。此时,执行线程2的CPU通过嗅探机制,能感知到共享变量已经被人修改了,所以就会把自己的高速缓存中的缓存行给失效掉。

正常情况下,线程2再次获取这个共享变量时,发现cpu-02的高速缓存失效,所以一定会从主内存中读取最新的值。

但是问题来了,如果线程2的运行速度非常快,线程1还没来得及把修改后的数据从高速缓存同步到主内存,线程2再次读取了这个共享变量的值,此时会发生什么呢?

此时的内存中存放的仍然是旧数据,那么线程2获取到的就是旧数据,并填充到了自己的高速缓存中,线程2自己甚至无法认识到错误,以为已经拿到了更新后的值… 这也太恶心了吧。

上述仅仅是我个人浅薄的思考过程,真正的MESI协议肯定不可能这么脆弱。

先说结论,如果线程1还没来得及把flag从高速缓存写回到主存,那么线程2是不能从主存中读取flag的!我刚才担心的场景不可能发生!

那么MESI协议是如何做到的呢?

cpu-01向L3发送消息,并被cpu-02嗅探到之后,cpu-02会把自己的高速缓存内的保存flag的缓存行的状态置为"I"。接着,线程2再次请求操作flag时,cpu-02发现待操作的flag在自己的缓存行内状态为"l",此时会进入等待状态,当且仅当cpu-01把缓存更新到内存后,cpu-02通过嗅探机制感知到,才能继续执行,从内存中读取最新的数据,读到自己的高速缓存,并修改缓存行的状态为"S"。

想想看也非常简单,cpu的缓存行不可能莫名其妙的变成"Invalid"状态,所以一定是其它的cpu修改了这条数据的。

5.2 写缓冲器

本来CPU是要通知并等其他所有的CPU核心都返回Invalid ack,现在不用等了,只要有修改操作,就把要修改的数据和内存地址写入到写缓冲区中,然后就可以继续干自己的事情了,等到自己空闲的时候,再去读取写缓冲区积压的消息,并通知其它的CPU核心,等到其它的CPU核心的Invalid ack都回复之后,再把写缓冲区的数据搬运到缓存行,做修改状态的操作、修改数据的操作,最后再把数据写回到内存。

如果这个CPU核心立刻读取这个内存地址的数据,能读到最新的数据吗?答案是能,因为现在在CPU核心和高速缓存之间,搞了一个写缓冲区,先读写缓冲区内的数据,读不到,再去读高速缓存。这个操作方式叫做存储转发

5.3 无效队列

过去,CPU核心收到无效通知后,需要立刻把指定内存地址的缓存行的状态置为无效化,然后还要向总线发一条Invalid ack。这么多的步骤,你难道不觉得这样做,返回一条ack的速度会很慢吗?如果此时这个CPU核心的负载和并发量很高,或者正在执行一个优先级非常高的任务,由于不停的接收这些烦人的消息,不得不停下手头上的事情,处理ack,你觉得这样好吗?

有了无效队列后,一切都不一样了。CPU核心收到无效通知后,只需要把消息写入到无效队列,接着立刻返回Invalid ack,速度非常快,接着继续做之前正在做的工作,等到闲时,再去读取无效队列,把缓存行无效化,这不就OK了吗。

5.4 写缓冲区和无效队列造成的问题

虽然这两个功能能够降低CPU修改共享数据时花费的时间,也能提高其它CPU返回invalid ack的速度,但是这个方案也会造成一些问题:

  1. 存储转发:cpu0 更新了a的值, 写到写缓冲器就往下走了,过一阵,要读a,这时先去写缓存器读, 读到的自以为是最新的,但是没准这一阵时间里面, cpu1已经改过a的值了,但是, cpu1发过来的无效通知, 是管不到 cpu0的写缓冲器的。
  2. 写缓冲器:其他CPU还没有给我们回答的时候我们已经执行下一步代码了。
  3. 无效化队列:其他CPU已经给对方应答的时候自己本身还没有去把这个值改为无效状态,这样就造成当前变量已经无效,但是通知还在无效队列化中,会取到旧值。

5.5 存储屏障和加载屏障

总之, 写缓冲器 ,无效化队列 就是导致了可见性问题, 明明写了 其他线程看不到这就需要编译器等底层系统 借助 内存屏障了。

  1. 存储屏障
    强制性的让cpu核心将写缓冲器排空,写入高速缓存 这叫冲刷, 这样其他cpu 就会收到通知, 其他cpu可以来拿新数据。
  2. 加载屏障
    强制性的让cpu 根据无效化队列里面的信息,删除其高速缓存的无效数据(就是状态变为Invalid)
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值