JUC并发相关

9. 并发终结之JMM

2020-09-19  本文已影响0人  涣涣虚心0215

Java Memory Model

JMM是隶属于JVM的,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化,也就是我们前面提到的处理器缓存
前面介绍并发三大特性里的有序性时提到,JIT以及处理器会进行指令重排序,并且基于处理器缓存还会有内存重排序
那么怎么解决重排序问题?答案是内存屏障
内存屏障Memory Barrier,是被插入到两个指令之间进行使用的,其作用是禁止编译器、处理器重排序从而保障有序性。除此之外,内存屏障还有一个附加功能:刷新处理器缓存和冲刷处理器缓存从而也保证了可见性。

内存屏障分类

MESI

首先我们知道CPU和主内存的处理速度相差很大,所以CPU设计中引入了Cache,在单核系统时代,只有一个core,就不需要任何内存屏障;但是在多核系统之后,每个core都有自己的Cache,Cache其实是主内存的副本拷贝,线程对变量的所有操作都是在Cache里面,而不能直接读写主内存,而且多核系统中,不同线程所在core的cache都是互相不可见的,这就需要一个缓存一致性协议来解决数据不一致问题。


image.png

MESI是一种缓存一致性协议,就是为了解决各个核心的Cache之间,对于同一个值的一致性问题。
Cache是分块的,每个块叫CacheLine,对Cache的操作都是以CacheLine为基本单位。
CacheLine有四种状态(M, E, S, I):

MESI协议规定如下:

MESI规定CPU之间的通信消息:

StoreBuffer

上面提到如果Core想更新自己的CacheLine,如果其CacheLine是M或者E,比较简单;如果状态是S,就需要通知别的Core的CacheLine状态需要设置成Invalidate,这个过程是需要等待ACK的,等待确认的过程会阻塞处理器的,降低处理器性能。
为此,CPU引入了StoreBuffer,则上面状态是S的时候,先把更新写到StoreBuffer,然后处理其他任务,等其他的Core的Invalidate ACK来的时候再把StoreBuffer写入Cache。
问题:StoreBuffer会带来一些问题,比如Core会尝试从StoreBuffer中读取值,又或者同步延迟导致执行顺序与代码顺序不一致。

    void executeByCPU0(void) {
        a=1;  //S1
        b=1;  //S2
    }
    void executeByCPU1(void) {
        while (b == 0) continue;  //L1
        assert(a == 1);           //L2
    }

如果Core0的Cache里不存在a值所对应的CacheLine,且存在b值的CacheLine,并且状态是E。
1 Core0先执行S1,发现a的CacheLine不存在,发送invalidate消息给Core1,同时更新a=1到StoreBuffer。
2 Core0接着执行S2,b对应的CacheLine是E状态,则直接修改Cache b=1,且CacheLine设为M。
3 这时候Core1开始执行,发现不存在b的CacheLine,于是广播read,获取b的值。
4 Core0监听到read信息,将b的值同步到主内存,并设置CacheLine的状态为S。
5 Core1得到b的值=1,L1通过。
6 Core1执行L2,a在Core1的CacheLine中并且是0,或者不存在,从主内存read也还是0,assert fail。
7 Core1收到第一步Core0的invalidate消息,设置a的CacheLine状态为I,并返回ACK,
8 Core0收到ACK之后,刷新StoreBuffer里面a的值到Cache,状态为M。(按道理Core1应该先设置状态I,并返回ACK,然后Core0刷新Cache,然后Core1执行L2的时候,再发read请求,这时Core0会将a的更新刷到主内存,这样Core1就能拿到a的更新)

InvalidateQueue

再看上面当收到Invalidate请求时,Core1需要停下来删除CacheLine之后再发送ACK才能继续任务。这也是降低处理器性能的地方。
为此引入InvalidateQueue来缩减Invalidate请求到等待ACK的时间,当收到Invalidate请求时,将请求放到InvalidateQueue,并立马返回ACK,再择机把InvalidateQueue中的失效CacheLine移除。
问题:InvalidateQueue也会带来一些问题,比如CacheLine移除时机变得不可确定,Core还有可能读到本应该移除的CacheLine里面旧的值。

    void executeByCPU0(void) {
        a=1;  //S1
        b=1;  //S2
    }
    void executeByCPU1(void) {
        while (b == 0) continue;  //L1
        assert(a == 1);           //L2
    }

还是同样的代码,继续分析:
如果Core0的Cache里不存在a值所对应的CacheLine,且存在b值的CacheLine,并且状态是E;Core1包含a的CacheLine,并且状态是E。
1 Core0先执行S1,发现a的CacheLine不存在,发送invalidate消息给Core1,同时更新a=1到StoreBuffer。
2 Core1返回ACK消息,并将Invalidate消息放到InvalidateQueue里面,继续开始其他任务。
3 Core0收到ACK将StoreBuffer的更新推送到Cache。
4 Core0执行S2,b对应的CacheLine是E状态,则直接修改Cache b=1,且CacheLine设为M。
5 Core1开始执行L1,发现不存在b的CacheLine,于是广播read,获取b的值。
6 Core0监听到read信息,将b的值同步到主内存,并设置CacheLine的状态为S。
7 Core1得到b的值=1,L1通过。
8 Core1执行L2,获取a的值,由于移除CacheLine的Invalidate消息还在InvalidateQueue中,所以a的值还在Cache,并且为0,导致assert fail。

解决方案

引入内存屏障:强制刷新StoreBuffer和InvalidateQueue

void executeByCPU0(void) {
    a=1;  //S1
    sfence(); //写屏障
    b=1;  //S2
}
void executeByCPU1(void) {
    while (b == 0) continue;  //L1
    lfence();//读屏障
    assert(a == 1);           //L2
}

内存屏障的指令:

参考

搞懂内存屏障-CPU的演进
缓存一致性协议之MESI

上一篇 下一篇

猜你喜欢

热点阅读