CPU的缓存模型
CPU的缓存模型:
CPU缓存模型下会存在并发问题:
1、线程1读取flag=0,并存在CPU缓存,线程2读取flag=0,并存在CPU缓存。
2、线程1将flag=0修改为flag=1,并刷新到到CPU缓存中,但不会立刻刷新到主内存中。
3、线程2感知不了flag已经修改为1,仍然认为flag=0。
缓存一致性协议(MESI协议)
MESI协议 :
保证在cpu缓存模型下不会出现多线程并发读写变量,没有办法及时的感知问题出现的机制。
对内存数据访问的控制类似于读写锁,它针对同一地址的读内存操作是并发的,而针对同一地址的写内存操作是独占的,一个处理器往内存中写数据时必须持有该数据的所有权。
处理器高速缓存的底层数据结构 :
拉链散列表的结构,每个bucket挂了很多的cache entry,每个cache entry由三个部分组成:tag、cache line和flag,其中的cache line就是缓存的数据(可以包含多个变量的值),tag指向了这个缓存数据在主内存中的数据的地址,flag标识了缓存行的状态。
处理器在读写高速缓存的时候,实际上会根据变量名执行一个内存地址解码的操作,解析出来index、tag和offset。index用于定位到拉链散列表中的某个bucket,tag是用于定位cache entry,offset是用于定位一个变量在cache line中的位置。
如果说可以成功定位到一个高速缓存中的数据,而且flag还标志着有效,则缓存命中。否则不满足上述条件,就是缓存未命中。如果是读数据未命中的话,会从主内存重新加载数据到高速缓存中。
MESI协议中一个缓存条目的Falg值有以下四种可能:
1、Invalid(无效):该状态表示相应缓存行中不包含任何内存地址对应的有效副本数据,该状态是缓存条目的初识状态。
2、Shared(共享):缓存条目的状态如果为Shared,如果其它处理器也存在相同Tag值的缓存条目,那么这个缓存条目的状态也为Shared,其缓存行中的数据与主内存中保持一样。
3、Exclusive(独占):其它所有处理器上的高速缓存都不保留该数据的有效副本,处于该状态下的缓存条目,其缓存行中的数据与主内存中保持一样。
4、Modified(更改过):处于该状态下的缓存条目,其缓存行中包含的数据与主内存中包含的数据不一致。
MESI协议定义了一组消息用于协调各个处理器的读、写内存操作:
处理器在执行内存读、写操作时在必要的情况下会往总线中发送特定的请求消息,同时每个处理器还嗅探总线中由其他处理器发出的请求消息并在一定条件下往总线中回复相应的响应消息。
消息名 | 消息类型 | 描述 |
---|---|---|
Read | 请求 | 通知其他处理器、主内存当前处理器准备读取某个数据,包含待请求数据的内存地址 |
Read Response | 响应 | 该消息包含被请求读取的数据 |
Invalidate | 请求 | 通知其他处理器删除指定内存的副本数据 |
Invalidate Acknowledge | 响应 | 接收到Invalidate消息的处理器必须回复该消息,以表示删除了其高速缓存上的相应副本数据 |
Read Invalidate | 请求 | 通知其他处理器当前处理器准备更新一个数据,并请求其他处理器删除其高速缓存中相应的副本数据 |
Writeback | 请求 | 该消息包含需要写入主内存的数据及其对应的内存地址 |
处理器对共享数据读操作的实现:
Processor0 | Processor1 | ||
---|---|---|---|
缓存条目 | 当前状态 | Invalid | Shared/Exclusive/Modified |
目标状态 | Shared | Shared | |
消息 | 发送 | Read消息 | Read Response消息 |
接收 | Read Response消息 | Read消息 |
Processor0读取数据A:
如果Processor0找到的缓存条目状态为Shared/Exclusive/Modified三者之一,那么该处理器可以直接从响应的缓存行中读取对应的地址,无须往总线中发送任何消息。
如果Processor0找到的缓存条目状态为Invalid,说明该处理器的高速缓存中不包含A有效副本数据,此时需要往总线发送Read请求读取数据,而其他处理器Processor1或者主内存则需要回复Read Response以提供数据。
Processor0收到Read Response消息时,会将其携带的数据存入对应的缓存行并将它的状态设置为Shared。
如果Processor1找到的缓存条目状态为Modified,那么Processor1可能往总线发送Read Response消息前将相应缓存行中的数据写入主内存,Processor1往总线发送Read Response消息后将缓存条目的状态设置为Shared。
如果Processor1找到的缓存条目状态为Invalid,那么Processor0接收的Read Response来自主内存。
处理器对共享数据写操作的实现:
Processor0 | Processor1 | ||||
---|---|---|---|---|---|
场景1 | 场景2 | 场景1 | 场景2 | ||
缓存条目 | 当前状态 | Shared | Invalid | Invalid | Modified/Exclusive/Shared |
目标状态 | Modified | Modified | Invalid | Invalid | |
消息 | 发送 | Invalidate | Invalidate | Invalidate Acknowledge | Read Response和Invalidate Acknowledge |
接收 | Invalidate Acknowledge | Read Response和Invalidate Acknowledge | Invalidate | Read Invalidate |
Processor0往地址A写数据:
Processor0找到的缓存条目的状态为Modified或者Exclusive,说明该处理器已经拥有响应数据的所有权,此时处理器可以直接把数据写入响应的缓存行并将相应的缓存条目状态设置为Modified。
Processor0找到的缓存条目的状态为Invalid,则处理器需要往总线发送Invalidate消息以获得数据的所有权,其他处理器收到Invalidate消息后会将其高速缓存中相应的缓存条目状态更新为Invalid(相当于删除)并回复Invalidate Acknowledge消息,收到所有消息回应后将数据更新到对应的缓存行中。
Processor0找到的缓存条目的状态为Shared,说明Processor1上的高速缓存可能也保留了地址A的数据副本,此时Processor0需要往总线发送Invalidate消息,Processor0收到其他处理器Invalidate Acknowledge消息回复后,将相应的缓存条目状态更新为Exclusive,获得地址A上数据的所有权,然后将数据写入缓存行,更新缓存条目的状态为Modified。
MESI协议的性能优化:
处理器执行写内存操作时,必须等待其他所有处理器将其高速缓存中的副本数据删除并接收到这些处理器锁回复的Read Response/Invalidate Acknowledge消息之后才能将数据写入高速缓存,为了规避和减少这种等地造成的写操作延迟,硬件设计者引入了写缓存器和无效化队列。
写缓存器和无效化队列都可能导致内存重排序。
写缓存器:
内存写操作的执行处理器在将写操作的相关数据写入写缓存区便认为些人完成,即该处理器并不等待其他处理器返回Read Response/Invalidate Acknowledge消息而是继续执行其他指令,减少写操作的延时。
无效化队列:
处理器在接收到Invalidate 消息之后并不删除消息中指定地址对应的副本数据,而是将消息存入无效化队列之后就回复Invalidate Acknowledge消息,从而减少了写操作执行处理器所需要的等待时间。
Java内存模型
伪共享
由于一个缓存行中可以存储多个变量的副本,因此即便是在两个线程各自仅访问各自的共享变量的情况下,一个线程更新其共享变量也可能导致另外一个线程访问其共享变量时产生缓存未命中,这种现象被称为伪共享。伪共享会导致缓存未命中,从而降低处理器执行内存读、写操作的效率。
在多个线程访问同一组共享变量的情况下,一个处理器上的线程更新了其中一个共享变量,会导致其他处理器上包含这个共享变量副本的缓存条目被无效化,缓存条目状态被置为Invalid,因此,这些处理器上运行的其他线程再次访问这个无效化的缓存条目的缓存行中曾经存有副本的任何一个共享变量时,都会产生缓存未命中。
消除伪共享的方法:
1、设法不让类似上面这些线程所访问的共享变量被加载到同一缓存行中,通过在类中添加一些无用的实例变量来干扰内存布局,以使特定的实例变量能够独自占用一个缓存行的空间。
2、JDK8提供了一个sun.misc.Contended注解,用来解决伪共享问题。