在前面JMM内存模型文章中,我有写到多个线程并发访问一个主内存的共享变量时,这些线程会在各自的工作内存中拷贝一份共享变量的副本,那么这就带来了一个问题,一个线程对共享变量进行修改后,其他的线程该如何感知到共享变量的改变从而做出适当的反应,确保后续线程读取这个共享变量的时候,总是最新的值,从而防止出现脏数据的情况。说到这,就不得不说一下缓存一致性的由来。
首先我这里放上一张CPU内核简易结构图
在最初的CPU都是单核心,然而在CPU的高速发展下,有那么一段话,CPU发展的速度有一个摩尔定律,几乎每过18个月就会更新一次,而内存就没有这样的定律,它每次发展就像挤牙膏一样,每次挤出一点,所以CPU更新迭代快它的处理速度就很快,而内存更新迭代慢所以处理的速度远远跟不上CPU处理的速度,且CPU每次从内存中拿取数据,需要通过系统总线拿取数据,这样就会导致性能严重的降低,为解决CPU与内存之间速率不匹配的问题,现代计算机系统中引入了缓存(Cache)用于提高性能,这样CPU就可以直接读取缓存中的数据。
以上图为例,在多核CPU中,每个内核都有自己的缓存,这就引来的一个问题,当缓存的数据与内存中的数据发生不一致的话该怎么办?于是就引来了缓存一致性协议啦。
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底层有很多涉及到硬件层面的内容啦,大家伙感兴趣可自行查阅相关文档哦~