JUC并发编程之MESI缓存一致协议详解

黎明大大

共 3088字,需浏览 7分钟

 · 2021-05-14

点击上方蓝字 关注我吧



1
前言

经过几篇文章,我一直在讲到并发下可能会导致很多问题的发生,通过volatile又能解决它的可见性和指令重排问题,在阅读我的文章的时候,不知道大家伙是否好奇过在计算机底层,它是如何保证数据的安全性的,volatile为什么能够解决这些问题?那么该文章就来用为简洁的话语来解释MESI协议。


2
为什么需要缓存一致性协议


在前面JMM内存模型文章中,我有写到多个线程并发访问一个主内存的共享变量时,这些线程会在各自的工作内存中拷贝一份共享变量的副本,那么这就带来了一个问题,一个线程对共享变量进行修改后,其他的线程该如何感知到共享变量的改变从而做出适当的反应,确保后续线程读取这个共享变量的时候,总是最新的值,从而防止出现脏数据的情况。说到这,就不得不说一下缓存一致性的由来。


3
缓存一致性的由来


首先我这里放上一张CPU内核简易结构图


在最初的CPU都是单核心,然而在CPU的高速发展下,有那么一段话,CPU发展的速度有一个摩尔定律,几乎每过18个月就会更新一次,而内存就没有这样的定律,它每次发展就像挤牙膏一样,每次挤出一点,所以CPU更新迭代快它的处理速度就很快,而内存更新迭代慢所以处理的速度远远跟不上CPU处理的速度,且CPU每次从内存中拿取数据,需要通过系统总线拿取数据,这样就会导致性能严重的降低,为解决CPU与内存之间速率不匹配的问题,现代计算机系统中引入了缓存(Cache)用于提高性能,这样CPU就可以直接读取缓存中的数据。


以上图为例,在多核CPU中,每个内核都有自己的缓存,这就引来的一个问题,当缓存的数据与内存中的数据发生不一致的话该怎么办?于是就引来了缓存一致性协议啦。


3
缓存一致性是什么


MESI(Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议,类似读写锁 对于同一地址的读内存操作是并发的,针对同一地址的写操作是独占的,对于内存地址写操作同一时间只能由一个处理器来执行。为了保持数据的一致性,MESI将缓存条目的状态划分为Modified.Exclusive,Shared,Invalid


MESI协议中一个缓存条目的状态Flag值分为以下四种

1. Invalid(无效的,记为I) 相应缓存行中不包含任何内存地址对应的有效副本数据,是缓存条目的初始状态
2. Shared(共享的,记为S)缓存行中包含相应内存地址数据的副本,其他处理器高速缓存中也可能包含相应地址内存的副本,缓存行中的数据与内存的一致
3. Exclusive(独占的,记为E)缓存行独占相应内存地址数据的副本,其他处理器高速缓存不包含相同的副本或者副本失效,缓存行中的数据与主内存数据一致
4. Modified(更改过,记为M)相应缓存行包含更新后的数据,其他处理器相同tag的缓存行只有唯一的M状态,与主内存的数据不一致

MESI定义了一组message用于协调各个处理器的读写内存操作,处理器在执行内存的读写操作时,在必要的情况下会往bus中发送特定的请求消息,每个处理器拦截这些消息,在一定情况下往bus回复消息。


如下图,就是MESI的概要图。


通过上面的理论,还不好解释MESI协议在CPU多核中到底是如何运用的,我会在下面通过几张图片的案例,来详细剖析它协议的转换过程。


如下案例,在内存中存在一个count共享变量,现在 "线程0" 需要用到该变量,那么 "线程0" 会从内存中通过bus总线将变量副本拷贝到缓存中,需要注意的是,线程都会对bus总线进行监听,例如 "线程0" 读取了共享变量,因为 "线程1"对bus总线进行了监听,所以它是知道的 "线程0" 进行了读取操作。但是目前为止该缓存变量它的状态为 "E(独占)",为什么为独占?因为该变量只被一个线程所使用。


接着,"线程1" 也需要用到该共享变量,它同样也会通过bus总线去内存中拷贝变量副本,这时 "线程0" 监听到 "线程1" 也拷贝了共享变量副本,此时 "线程0" 它会将内部的变量状态标识改为 "S(共享)",而 "线程1" 这边也会将变量状态标识为 "S(共享)" ,那么此时线程就不能够随便的对变量进行修改了,因为该变量被多个线程所使用,所以CPU需要同时对两个线程中的变量进行维护。


然后接着,"线程1" 对变量了进行修改,此时 "线程1" 会将共享变量的状态标识改为 "M(修改)",并通知bus总线该变量已经发生了修改,那么这时候 "线程0" 监听到了 "线程1" 修改了共享变量, "线程0" 就会将变量状态标识改为 "I(丢弃)",当CPU识别到 "线程1" 变量状态标识为i的时候,就会将该变量从缓存中进行丢弃,重新去内存中拷贝最新的变量副本。


但是此刻会衍生出另外一个问题,假如两个线程同时进行对变量进行了修改,那么到底是哪个线程修改成功呢?还是说晚修改的变量会对早修改的变量就行覆盖?那这样岂不是会造成脏数据的发生?
针对以上这种情况当然是不允许发生的啦,如果线程需要对变量进行修改,会先在本地的缓存行中上一个lock锁(本地写缓存行),因为数据都是存放在缓存行中的,但是会有这么一个问题,它们是对各自的缓存行进行上锁,其他的线程是并不知道,还是无法解决多个线程同时操作造成脏数据的发生,但是CPU也考虑到缓存一致性的问题,假如多个线程都对各自的缓存行进行了上锁,也同时发送本地写缓存行消息给了bus总线,那么此时就会由bus总线来决定,由某个线程来进行修改。


以上是针对正常的情况,MESI协议能够正常的对缓存行进行状态标识转换,那么我们来聊一聊针对非正常情况,MESI协议是否还适用呢?
在CPU缓存中,它的缓存大小为64个字节,假如我们现在内存中有一个大对象,它的大小为124个字节,那么在CPU中一个缓存行是无法进行存储的,它会将变量存储在两个缓存中,这样的话CPU在对变量进行操作就不再是原子操作,MESI协议无法同时对线程内的两个缓存行进行lock加锁,这时MESI协议失效,缓存行锁失效,从而晋升到bus总线锁。


总线锁的概念:将总线锁住,只有一个内核线程能够操作该变量,其他的线程只能默默的看着它进行操作啦,它的坏处在于,由多核的CPU变成了单核CPU操作效率大幅度的降低。


不知道大家伙看到这,对CPU底层对多线程数据处理,以及它的安全性问题是否有一个比较清晰的认知了呢?关于CPU底层有很多涉及到硬件层面的内容啦,大家伙感兴趣可自行查阅相关文档哦~


我是黎明大大,我知道我没有惊世的才华,也没有超于凡人的能力,但毕竟我还有一个不屈服,敢于选择向命运冲锋的灵魂,和一个就是伤痕累累也要义无反顾走下去的心。


如果您觉得本文对您有帮助,还请关注点赞一波,后期将不间断更新更多技术文章


扫描二维码关注我
不定期更新技术文章哦



JUC并发编程之单例模式双重检验锁陷阱

JUC并发编程之Volatile关键字详解

JUC并发编程之JMM内存模型详解

深入Hotspot源码与Linux内核理解NIO与Epoll

基于Python爬虫爬取有道翻译实现翻译功能

JAVA集合之ArrayList源码分析

Mysql几种join连接算法



发现“在看”和“赞”了吗,因为你的点赞,让我元气满满哦
浏览 3
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报