1 问题引出
-
现在的电脑大多都是多核的CPU可以真正的做到并行
-
CPU执行指令的速度远远大于从内存中IO数据和指令的速度,这就会造成CPU资源的浪费,所以出现了高速缓存来解决这一问题,每个cpu有自己的高速缓存, 将内存中的数据先copy到各个cpu的高速缓存中,然后cpu直接从高速缓存中获取数据和指令来执行,然后在将结果同步到内存当中。
-
问题出现 :如下图 同一个共享变量(stopFlag)被先后 copy 到不同的cpu的高速缓存中但是B cpu执行了修改指令,这是A cpu 还是copy到原始的值,并没有办法感知到。所以就会出现问题(无限循环,程序无法结束)
-
整体执行步骤 (下图辅助理解 ,图片过大直接看不太清晰 请点击查看)
- 指令及数据加载到内存
- 指令和数据被加载到各个cpu的高速缓存
- cpu执行指令并把结果写入到高速缓存
- 高速缓存将结果同步到内存
-
总结概念 : 多核cpu下,不同的cpu 操作了同一个共享变量,但彼此是并不是实时的可见的,会导致程序出现问题
2 解决方案
-
添加总线锁 : 在多核cpu下,添加总线锁之后当其中一个cpuA对共享内存数据进行操作时,在总线上发出一个 LOCK 信号,其它cpu无法通过总线来和内存进行通信,相当于锁住了其它cpu和内存进行通信的通道,cpuA 操作完成之后才会释放总线锁.各个cpu之间对共享数据操作变成了同步的了,所以能够保证安全,但是开销太大,造成资源浪费.效率低下.
-
缓存一致性协议MESI : 为了保证在多核cpu下不会出现像上述那种问题造成系统数据混乱,所有引入了 缓存一致性协议 ()
- 缓存行基础概念 : 每个高速缓存是由许多个缓存行组成的,真正保存高速缓存中数据的存储单元.
- MESI 实际对应缓存行的四种状态
- M修改状态 (Modified) : 当前缓存中的数据被修改了,和内存中的数据不一致,并且数据只存在于当前cpu的缓存中. (当前缓存行必须时刻监听是否有其他cpu缓存读取内存中的该变量,如果有则必须在当前缓存同步到内存前延迟操作)
- E 独占状态(Exclusive) : 数据值存在于当前cpu的缓存中,数据和内存中一致. (当前缓存行也需要监听是否有其他cpu缓存读取内存中的该变量,如果有则该缓存行需要变成共享状态)
- S 共享状态(Shared): 数据存在多个cpu的缓存中,各个缓存中数据和内存中一致. (需要监听其它缓存发出的使当前缓存数据无效的请求,如果有则需要将该缓存数据变成无效)
- I 失效状态(Invalid):数据在当前cpu的缓存中失效.
-
MESI 大致运行流程图如下 (图片过大直接看不太清晰 请点击查看)
3 缓存一致性协议所带来的问题
虽然缓存一致性协议解决了多核cpu下的缓存一致性问题,但是同时也带来了其它问题,存在于多个cpu缓存中的共享变量,如果一个cpu对其进行了修改,则需要对其它cpu发出失效通知,这时对于当前cpu是阻塞的直到其它cpu应答失效通知后才会继续后面的操作,但这个等待的过程比执行一个简单的指令所耗费的时间要高出几个数量级,造成了cpu资源的浪费,所以硬件设计引入了 “store buffer”,它位于cpu 和 高速缓存之间。如果进行上述更改操作时,则cpu直接将数据写入 store buffer 不会在进行等待,直接去做其它的事情,当失效通知被其它cpu确认完成之后,store buffer中修改的数据才会被提交。
- Store buffer 引入同样也会带来一个很明显的问题 如下图
- 由于store buffer引入,cpu不会等待失效其它cpu失效应答,直接指向后续操作,可能之前的失效应答还没有被当前cpu收到,导致修改后的数据没有被提交,造成了修改后的数据对后续操作不可见.导致出现问题,所以硬件上又引入了 store forwarding 的概念,当执行读取操作时会先从 stor buffer中读取,如果stor buffer有则不经过高速缓存,如果没有则再去高速缓存中读取.这样就不会出现上述问题., 但还是会有问题如下
- Cpu无法知道变量之间的关联关系,所以对于这类问题硬件上也无法能够解决,但是硬件上提供了内存屏障 memory barrier指令,让用户来告诉cpu这类关系
- 但还是会有问题 cpu的store buffer 很小,几个store操作之后store buffer就会满了,这时cpu 必须等待其它cpu的失效应答之后,才会释放store buffer空间,得到失效应答的变量同步到高速缓存中.将该变量移除store buffer.所以硬件上引入了失效队列 (Invalidate Queues),各个cpu收到失效通知后,将该变量直接放入失效队列并应答失效通知,但这并不是真正的失效,而是等待后续cpu对失效队列里的变量执行失效操作.但有可能出现下图中的问题.
- 对cpuB读取a之前,应该讲失效队列a的失效今天提交,则这样就不会出现问题.如下图
- Memory barrier 可以细分为两种,一个是 write memory barrier 针对于 store buffer 强制提交store buffer 中的变量,还有一种是读屏障 针对的是 invaldate queue ,强制提交失效队列里的变量失效.
4 总结
缓存一直性协议是为了保证并发场景下线程可见性(实际所说的cpu 指令重排序并不是真正的排序,也是由于共享变量不是实时可见导致的),实际上是对 汇编指令上加了 锁操作的指令的变量进行应用, 表现到 java 层面是使用 volatile 关键字修饰的变量.
笔者第一次写博客排版上可能不怎么清晰,还请谅解。由于技术水平有限,可能文中有些地方表达的也不是那么准确,或者有可能有些地方存在问题,如果有什么问题还请各位大佬指出。或者觉得哪里有问题都可以指出。 谢谢。
参考文章:
- https://www.cnblogs.com/ynyhl/p/12119690.html (主要讲 MESI 流程)
- https://blog.csdn.net/Design407/article/details/103392731 (MESI 变化过程,store buffer 引入原因)
- https://blog.csdn.net/chen19870707/article/details/39896655 (内存屏障引入的原因)