13.深入浅出高速缓存带来的可见性问题

除了编译器优化等导致重排序,还有一种情况也会导致可见性问题,那就是高速缓存的存在,本节我们就来分析一下。

本节涉及的内容,例如缓存一致性协议等,在网上能找到很多介绍,但是其转移过程都非常复杂,我们重点深入浅出解释该问题。

1 总线锁和缓存锁

我们知道在计算机中CPU是速度最快的,而磁盘IO等是相对比较慢的,因此为了提高CPU的处理效率会在多个层次增加缓存。首先是CPU内核就有寄存器作为缓存,另外还有数据缓存和指令缓存。这两级都是在CPU内的,而CPU和IO设备之间还有一层缓存,这就是CPU的三级缓存。如下图所示:

这时候很明显的问题就出现了,如果一个CPU修改了数据,而其他CPU并不知道,就可能使用错误的数据继续进行其他处理,那么自然结果就错了。这就是CPU的缓存导致一致性出现问题的原因。

为了解决该问题就有两种基本的方法:总线锁和缓存锁,而缓存锁主要是指缓存一致性协议。

我们先明确一下什么是总线,所谓总线就是CPU与内存、IO设备之间的公共通道,也就是上图中蓝色标记的区域。当CPU访问内存时,必须经过总线锁。

而总线锁就是在总线上声明一个Lock#信号,这个信号能保证共享内存只有当前CPU可以访问,其他的处理器请求时都会被阻塞,这样就可以保证同一个时刻只有一个处理器能访问共享内存,从而解决了不一致的问题。这么做的代码也很明显,就是CPU的利用率严重下降。

为此,处理器又推出了缓存锁机制。意思是说如果当前CPU访问的数据已经缓存在其他CPU的高速缓存中,那么CPU不会总再总线上声明Lock#信号,而是采用缓存一致性协议来保证多个CPU的缓存一致性问题。

2.缓存一致性协议

缓存锁是通过缓存一致性协议来保证缓存一致性的,不同CPU支持的类型有所差异,目前使用最多的是MESI协议,该协议被应用在Intel奔腾系列的CPU中。

MESI是Modified、Exclusive、Shared、Invalid这四个单词的首字母。这4个字母分别代表4种状态:该协议的原理很简单,就是在MESI协议中,定义了几个不同情况下的状态,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移。

状态描述
Modified(修改)这行数据有效,数据被修改了,和内存中的数据不一样,数据只存在于本cache中。
Exclusive(互斥)这行数据有效,数据和内存中的数据一致,数据只存下于本Cache中  
Shared(共享)这行数据有效,数据和内存中的数据一致,数据存在于很多cache中  
Invalid(无效)这行数据无效

我们结合图示再看一下上述几种状态是啥意思。

E状态

只有Core 0访问变量x,它的Cache line状态为E(Exclusive)。

S状态

3个Core都访问变量x,它们对应的Cache line为S(Shared)状态。

M状态和<I>状态之间的转化

Core 0修改了x的值之后,这个Cache line变成了M(Modified)状态,其他Core对应的Cache line变成了I(Invalid)状态。

状态明确之后,那相互之间是如何迁移的呢?这个过程描述起来非常复杂,我们只看简化的情况:

假如只有两个CPU核,如图1所示,当单个CPU从主内存中读取一个数据保存到高速缓存中时,具体的流程是CPU0发出从内存中读取x变量的指令,主内存通过总线返回数据后缓存到CPU0的高速缓存中 ,并且设置该缓存状态为E。

此时如果CPU1同样发出一条针对x的读取指令,如图2所示,那么当CPU0检测到时会针对该消息做出响应,将缓存在CPU0里的x通过Read Response消息返回给CPU1,此时x分别存储在CPU0和CPU1的高速缓存中,所以x的状态被设置为S。

然后CPU0把x变量的值修改成x=30,把自己的缓存状态设置为E,接着把修改后的值写入内存,此时x的缓存行是共享状态,同时需要发送一个Invalidate消息给其他缓存,CPU1收到该消息之后,把高速缓存中的x设置为Invalid,最终得到如下的结构:

 也许这里你会有个疑问,图3中Cache的状态应该是E还是M呢?我的理解是如果将x=30同步到内存之前就是M,同步到内存之后就是E。

根据上面的描述,我们可以看到,CPU的高速缓存导致了缓存一致性的问题,为此,CPU层面提供了总线锁和缓存锁的机制。基本思想是通过LOCK#信号触发总线锁和缓存锁,如果不支持总线锁。则会使用缓存一致性协议来保证缓存一致性协议。其目标都是为了保证同一时刻只允许一个CPU堆共享内存进行读写操作。

总结

说了这么多,那volatile到底做了什么呢?其实就是JVM看到某个变量有volatile之后,调了一下底层的Lock#指令而已。其他的都交给CPU和总线等来处理了。

说了这么多,与volatile有啥关系呢?Volatile一个关键字将上面的内容全给实现了,或者调用底层的服务来实现了。我们可以看一下,当我们对一个变量加了volatile关键字之后在不同的层次会有什么变化,具体包括:

  • 代码层面: volatile关键字

  • 字节码层面:ACC_VOLATILE字段访问标识符

  • JVM层面:JMM要求实现为内存屏障。

  • (Hospot)系统底层: 读volatile基于c++的volatile关键字,每次从主存中读取。 写volatile基于c++的volatile关键字和 lock 指令的内存屏障,每次将新值刷新到主存,同时其他cpu缓存的值失效。 C++的volatile禁止对这个变量相关的代码进行乱序优化(重排序),也就具有内存屏障的作用了。

  • Linux内核也可以手动插入内存屏障:_ asm _ _ volatile _ ( " " : : : "memory" )。这样就控制CPU的执行按照的执行了。

拓展

这里补充一个概念,上面图中我们用的是缓存行而不是缓存,而且这里会引申出另外一个问题——伪共享问题,感兴趣的同学可以研究一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值