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

共 3088字,需浏览 7分钟

 ·

2021-05-14 16:46

点击上方蓝字 关注我吧



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连接算法



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

手机扫一扫分享

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

手机扫一扫分享

分享
举报