【进阶篇】线程的硬件基础

文章目录

    • 高速缓存
    • 缓存一致性协议
    • 写缓冲区和无效化队列
    • 再识内存重排序
    • 再识可见性

高速缓存

简介
高速缓存是主内存与处理器之间的硬件,其容量小于主存,但存取速率远高于主存。因此处理器在执行读写操作时,可直接和高速缓存交互,提高响应速度。
我们常见的变量名相当于内存地址,变量值相当于内存中的数据,而高速缓存相当于是为每个变量保留了一份副本。但其容量较小,不能长时间保存。

数据结构
高速缓存相当于一个容量极小的hashTable,key是内存地址,value是变量的值。从结构上来看,其由缓存条目组成。其结构大致如下:
【进阶篇】线程的硬件基础_第1张图片
每一个缓存条目可继续划分为Tag、DataBlock、Flag三部分。DataBlock被称为缓存行,它是高速缓存与主存之间数据交换的最小单元;Tag包含了与缓存行中数据内存地址的部分信息;Flag用来表示缓存行的状态信息。见下图:
【进阶篇】线程的硬件基础_第2张图片
缓存命中
现在来简单说一下缓存命中的流程;当处理器在进行读取时,会先进行内存地址解码操作,解码结果包括tag、index、offset三部分数据;index相当于桶的编号,用来定位内存结构中桶的编号;tag用来定位桶对应的缓存条目,根据缓存条目的Tag进行比较;offset时缓存条目里缓存行内的位置偏移量,它用来定位一个变量在一个缓存行中存储起始位置。根据这三个来定位缓存数据,如果能找到缓存条目中的Flag,则说明缓存命中了,否则为缓存未命中。

现在处理器一般都具有多个层次的高速缓存,分为一级缓存、二级缓存、三级缓存等,一级缓存集中在cpu的内核中,访问效率极高,一般分为两部分,一部分用于存储指令,另一部分用于存储数据。离cpu越近的高速缓存,存取速率越快,但其制造成本也就越高,因此容量越小。
(图片来源百度)

缓存一致性协议

当多个线程在访问同一变量时,其中一个线程更新了该变量,需要其他线程立刻察觉到。为了解决这个问题,处理器之间需要一种通信机制------缓存一致性协议。
MESI(Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议。它可以保证多个线程在读共享数据时是支持并发的,但写操作是独占的。
正如它的名字一样,MESI将缓存条目状态划分为如下4种,并在此基础上定义了一组消息用于协调各个处理器读写内存的操作。
一个缓存条目中的Flag值具有以下4种可能:

状态 含义 是否与其他处理器中缓存中值一致 是否与主存中值一致
Invalid(无效的,标记为I) 该状态表示缓存行中不包含任何内存地址对应的值,也就是缓存未命中,它是缓存条目的初始状态。
Shared(共享的,标记为S 该状态表示缓存行中存在相应内存地址的变量值的副本。且其他处理器缓存中可能也具有相同的副本。因此,如果状态为Shared,则说明其他处理器中高速缓存的值与本处理器缓存的值一样,且都为Shared。该状态表示当前处理器缓存中的值与主存一致。
Exclusive(独占的,标记为E) 该状态表示缓存行存在相应内存地址的变量值的副本。且该处理器以独占的方式保留了内存地址数据的副本,其他处理器缓存中不具有该副本。该状态表示当前处理器缓存中的值与主存一致。
Modified(更改过的,标记为M 该状态表示相应缓存行中存在内存地址更新后的数据。由于MESI协议只能在同一时刻有一个处理器对主存进行更新操作,因此同一时刻,多个处理器中只能有一个处理器中的缓存条目是该状态。该状态的缓存条目,其中缓存数据与主内存中的数据不一致。

现在来描述一下使用MESI协议的处理器是如何读写操作的。假设内存地址A上的数据为S可能是处理器P1和P2共享的数据。
先来看下MESI中的消息体的消息类型:

消息名称 消息类型 描述
read 请求 通知其他处理器、主存,表示当前处理器准备读取地址中的数据。该消息包含待读取数据中的内存地址
Read Response 响应 该消息由主存或者其他处理器提供,包含被请求读取的数据。
Invalidate 请求 通知其他处理器将对应的缓存条目状态置为I,表示删除指定内存地址的副本数据
Invalidate Acknowledge 响应 接收到Invalidate消息的处理器必须回复该消息,表示删除了其高速缓存上相应的副本数据
Read Invalidate 请求 该消息是由Read 和Invalidate消息组合的复合消息。告知其他处理器要更新一个数据,并且要其他处理器删除其高速缓存中相应的副本数据。

读取操作:
当P0要读取数据S时,会根据地址A找到本处理器上的缓存条目,如果P0找到的缓存条目中的Flag为M、S、E,则P0可以直接读取本处理器中地址A对应的数据S,其无需向总线中发送任何消息。如果P0找到的缓存条目为I,则说明本处理器中高速缓存不存在S的副本,此时需要向总线中发送Read消息来读取地址A的数据,其他处理器P1或者主存需要回复Read Response以提供相应的数据。
P0接收到Read Response 时,会将其中携带的数据S存入相应的缓存行中并将缓存条目中的状态更新为S。当P0发送Read时,P1会嗅探总线中的消息,然后从消息体中取出待读取的内存地址,找到本处理器中的缓存条目,如果状态不为I,则说明存在数据的副本,则P1构造 Read Respone消息并将数据副本所在的整块数据塞入消息中。
如果P1找到的相应缓存条目状态为M,则P1可能在向Read Response消息前将相应缓存行中的数据写入主内存,先保证主内存中的数据时最新的。发送完Read Response后,相应的缓存条目状态会更新为S。
如果P1找到的相应缓存条目状态为I,则P1不做任何处理,发送Read Response消息的可能是主内存。

写入操作:
当P0向地址A中写数据时,它先会根据A来找到本处理器中的缓存条目,如果缓存条目的状态为E或者M,则说明该处理器已经拥有了该数据的写权限,则P0会将数据直接写入到缓存行中,并将缓存条目更新为M。
如果P0找到的缓存条目状态不为E、M,则需要向总线中发送Invalidate消息来获取数据的所有权,其他处理器接收到Invalidate消息后会将本处理器高速缓存相应的缓存条目状态更新为I(相当于删除变量的副本)并回复Invalidate Acknowledge消息。P0必须在接收到所有处理器的消息后才能更新缓存条目。
如果P0找到的状态为S,说明P1的高速缓存可能也保留了A对应的数据副本。此时P0需要向总线中发送Invalidate消息,在接收到所有处理器回复的Invalidate Acknowledge 消息之后会将相应的缓存条目状态改为E,然后将数据写入相应的缓存条目,之后把状态更新为M。
如果P0找到的状态为I,则表示处理器不包含地址A对应的数据副本,此时P0需要向总线中发送Read Invalidate消息,P0在接收到所有处理器返回的 Read Response和Invalidate Acknowledge 消息之后会将相应的缓存条目的状态更新为E,然后将数据写入相应的缓存条目,之后把状态更新为M。

写缓冲区和无效化队列

在使用MESI协议时,处理器执行内存操作,必须等待其他所有处理器将本地高速缓存中对应的副本删除,并接收到删除的消息之后才能将数据写入到高速缓存中。为了规避或减少这种等待造成的写操作的延迟,所以引入了写缓冲器和无效化队列。

写缓冲器(Store Buffer)是处理器内部一个容量比高速缓存还小的硬件。进行写操作时,整体的流程如下:
如果对应的缓存条目为E或者M,则处理器直接将数据写入相应的缓存行即可。如果为S,则处理器会先将数据写入到写缓冲器的条目中,并发送Invalidate消息。如果是I,则该写操作是未命中的,此时处理器会先将数据写入到写缓冲器中,发送Read Invalidate消息。当处理器将数据写入到写缓冲器之后就认为写操作已经完成,且并不等待其他处理器返回的Invalidate Acknowledge 消息,就可以执行其他命令,比如读操作。等到当前处理器接收到其他处理器返回的Invalidate Acknowledge 消息后,再将数据写入到相应的缓存行中。
可见,写缓冲器的引入可以不用等待消息的返回,从而减少了写操作的延迟,这使得本处理器再等待消息的时间内可以做其他的事情。
由于写缓冲器的引入,处理器在执行读操作时,会先从写缓冲器中读取,目的是防止缓存行中的数据是旧值(因为先写入的地方是写缓冲器),这种技术叫做存储转发。

无效化队列(Invalidate Queue)的作用是在其他处理器接收到Invalidate消息后并不删除消息中指定地址的副本数据,而是将消息存入到无效化队列中之后就回复 Invalidate Acknowlege,从而减少了写操作执行处理所需的等待时间。

再识内存重排序

P0 P1
X=1 //L1 Y=1 //L2
r1=Y; //L3
r2=X //L4

假设P0和P1两个线程执行的顺序按照上表执行,X、Y为共享变量,初始值都为0,r1、r2为局部变量。当P0执行到L3时,虽然L2已经被P1执行完毕,但是由于L2的执行结果可能还在P1的写缓冲区中,所以P0再执行L3时,读到的变量是0。同理,P1在执行L4时,P0对X1的写的内容也可能还在P0的写缓冲区中,因此P1读到的也可能是0。因此,从P1的角度来看,P1执行L4那一刻P0已经执行完成了L3,但L1好像未执行,对P1来说,P0的执行顺序是L3->L1,也就是说写缓冲器导致了重排序。

再识可见性

之前的这篇博客简单总结了volatile可见性的原因,可提前了解。

因为写缓冲区和无效化队列的引入,会导致可见性的问题。
A处理器上的线程更新了一个共享变量,B处理器的线程进行读取时,可能读到的是更新之前的值,原因是A处理器上的线程将变量的值仅仅写入到了写缓冲器当中,并未同步到B处理器中的高速缓存和主存当中。
处理器在一些特定条件下,比如写缓冲器满了,会将写缓冲器冲刷,将内容写入到高速缓存中,但是这种情况可能并不及时。因此要保证可见性需要借助内存屏障中的存储屏障,该屏障作用就是进行冲刷写缓冲区。

冲刷了写缓冲器只解决了可见性一半的问题,可见性的另一半原因是无效化队列导致的。
如果B在接受到A的Read Invalidate消息后,只是将消息存入到了无效化队列,没有把高速缓存中的副本删除,那么B读取的还是旧数据。内存屏障中的加载屏障用来解决这个问题。加载屏障会根据无效化队列中的内容将高速缓存中的条目状态改为I,从而使该处理器后续的读操作时,必须发送Read消息,以读取其他处理器中最新的值。

总结一下,导致可见性的原因分为两个维度,一是A处理器没有发消息,二是B处理器没有处理消息。

你可能感兴趣的:(多线程,java,缓存,java,开发语言,jvm,面试)