java共享缓存同步_java并发-多核CPU缓存架构及MESI缓存一致性协议

  • Post author:
  • Post category:java


摘要

之前讲解过计算机理论模型:计算器、控制器、存储器、输入设备、输出设备。计算机硬件结构:cpu硬件结构(寄存器+多级缓存+总线接口)、总线(I/O总线、内存总线)、控制器(USB控制器,磁盘控制器)、存储器(总存储器、内存拓展槽)等;以及cpu内部结构(控制单元【指令存储器+指令计数器】、运算单元、存储单元);既然讲解了cpu内部结构以及硬件结构;这一节主要讲解多核CPU缓存架构一节实现缓存一致性的协议个规则。

思维导图

d2456491c4d35cfdd8538c3f9da961fa.png

内容

1、cpu多级缓存

cpu多级缓存架构: cpu多级缓存是真实存在的硬件设计结构,一般多级缓存结构如下所示:

d46ac9fd4ba732fe1b89e5a76378e50b.png

程序核心执行流程: cpu通过总线从主内存里面读取指令跟需要运算的数据。通过总线复制到多级缓存当中、然后加载到寄存器当中;指令寄存器根据加载的指令进行计算,计算之后将变更的数据存储到寄存器,然后刷入到L1->L2->L3 然后会刷入到主内存。我们假如有多核cpu的话,我们的主内存数据就会有多个副本在哥各个cpu里面。

cpu多级缓存结构存在的问题: cpu多级缓存存在数据不一致的问题;主要原因是因为各个cpu进行算数运算跟逻辑运算读写数据都是先从主内存加载数据之后,都是针对于自己的cpu缓存进行操作的。

我们先查看下如下代码:/**

* CPU多级缓存问题 */public class CPUMultiLevelCacheDemo {

/**

* 主内存里面的变量 */ private static int x = 0;

public static void main(String[] args) {

new Thread(()->{

for(;;){

System.out.println(Thread.currentThread().getName()+”读取数据x:”+x);

x++;

System.out.println(Thread.currentThread().getName()+”写数据x:”+x);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

},”线程1″).start();

new Thread(()->{

for(;;){

System.out.println(Thread.currentThread().getName()+”读取数据x:”+x);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

},”线程2″).start();

}

}

我们发现输出结果为:

e9a3bf612d78d7579d846099b71e7474.png

我们发现线程1读写一直是正确的,因为是操作的本线程的工作内存里面的东西;但是线程2操作的数据却不正确;线程2是看不到线程1的数据。

我们分析如下:

比如上面我们在主内存有一个变量:x=0;然后程序指令是进行加1的操作。

比如我们在cpu1有一个线程1,cpu2上有一个线程2;两个线程都会将变量

X=0复制到各自缓存中。cpu进行计算的时候,会从自己的寄存器里面查询数据。如果寄存器里面没有数据的话,他会从cpu多级缓存一次查找:L1、L2、L3;如果缓存中也没有的话,就从主内存里面去查找。然后依次往上复制,直到寄存器里面。如果我们线程1进行了x=0加1,这样的话两个线程2读取对应的x=1;接着将数据刷新.但是最终出现了线程2加载的数据不是线程1更改的数据;这种副本复制导致数据不一致。

2、cpu多级缓存结构存在的问题

多级cpu会存在缓存不一致的问题,基于这个问题,是由于各个cpu会优先从自己的缓存加载数据,并将数据写到自己的缓存中去,所以导致数据不一致;为了解决这个问题,cpu设计了两种方式:总线加锁、缓存一致性方式。

总线加锁: 当我们的cpu1先访问我们主内存的一块数据时候,cpu1先对总线进行加锁。加了锁之后,cpu2就不能通过总线对主内存进行访问了。这样的话访问变成了串行化;导致我们的多核cpu的优势发挥不出来。大多数情况下我们不使用这种方式,而是采用缓存一致性协议方式。当缓存一致性协议解决不了的情况下我们也会使用总线加锁协议进行替换。

3、缓存一致性原理

缓存一致性协议除了有MESI协议之外还会有其他协议的,大多数是MESI。

涉及到的概念:cpu多级缓存进行存储的最小存储单元就是Cache line(缓存行)。

缓存行Cache Line的4种状态: MESI(M-Modified E-Exclusive S-Shared I-Invalid)。

5f87857ff01cb8d251d5f113b3729e5c.png

M-表示修改:就是说我们修改的缓存行数据有效情况下,数据被修改了 导致跟内存中的数据不一致;而且现在修改后的数据只存在与本地缓存中;这个时候就涉及到需要锁这个缓存行;并且向我们总线发一条消息,其他的cpu会监听总线发出的消息。

假设有2个cpu: cpu1跟cpu2 对应的缓存为cache1跟cahce2

在主内存定义了一个变量x=2;

初始化状态: 初始化时候,cpu1跟cpu2以及主内存数据如下:

62026c9b3d0a1d9f0db54de0c5b0f48e.png

cpu1跟cpu2中的cahce中不曾有数据。主内存数据x=1;

cpu1初始化读数据:

cpu1通过bus总线读取数据到cache1,此时数据cache1的状态:E,然后缓存行监听嗅探总线。

801324c0e5578102b4687b9931e76933.png

cpu2初始化读数据:

e667825bfa14dd23aa748970aad05d17.png

cpu1在已经将数据读取到缓存的情况下,cpu2通过bus总线此时读取主内存的数据,此时cpu1嗅探到cpu2读取数据,所以数据状态为:E->S;cpu2读取数据到cache2的时候也是为:S;

cpu1修改数据:

a5b64f6c729e76926e347925052f4f2d.png

cpu1修改数据,此时cpu本地缓存数据状态有效,数据与主内存数据不一致。然后将缓存行数据修改成E->M;并且通知发布修改通知总线;其他cpu跟嗅探到x时候,会将此数据过期。

cpu2查询数据时候,cpu1同步数据到缓存/主内存:

0c9493a38dd4d13cdc8637e4ca4944c0.png

cpu2查询数据时候,会延迟等待cpu1同步数据到缓存cache2/主内存;此时cache1的状态为:M->E;然后cpu2加载数据到cache2时候;x=2 状态为:I->S;然后此时cpu1的数据状态变化为:E->S;

注意:上面我们cpu1将x同步到内存里面的时候,其对应的多级缓存里面的缓存行状态是E;只要当cpu2如果读取数据时候,这个时候cpu1里面的x数据缓存行状态才会是S状态。(只有cpu读取时候变量共享才会变成S状态) 。

疑问?

上面我们讲解的都是cpu1跟cpu2之间有一个先后顺序的状态:并发读取和写入有先后状态;假如我们cpu1跟cpu2同一时间都发出一个M修改的指令呢?在MESI里面不会出现这种情况的,在一个指令周期内,会有一个指令裁决、硬件方面的支持。裁决成功是M状态,失败的话是I状态。不存在多个M状态。裁决失败的话会不会再去读取呢?取决于程序指令是否继续需要读取,比如CAS无效重试while true;

缓存一致性协议并非所有情况都符合、在以下情况下不符合:

1、变量对象比较大,在一个Cache line里面存储不下。可能要多个缓存行,锁不住这个变量了,这个时候只能总线加锁机制了。

2、Cpu本身不支持MESI协议的。比如早起奔腾等。

因此真正的缓存一致性原理如下:

c58313d6f2e749b1236040bda123ca64.png



版权声明:本文为weixin_35990295原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。