前言
王子之前的文章对于并发编程中的可见性问题已经有了一个初步的介绍,总结出来就是CPU的缓存会导致可见性问题。
这样的解释其实是没有问题的,但这里说的“缓存”其实一个笼统的概念,缓存其实指的是寄存器、高速缓存和写缓冲器。
今天我们就从硬件的级别再来探索一下出现可见性问题的原因,让小伙伴们有一个更深的认识。
同时再深入探索一下有序性问题的产生原因。
如果小伙伴们对于寄存器、高速缓存、缓冲器、总线的概念还不清楚,建议自行去查阅资料了解。
出现可见性问题的原因
首先,我们知道每个CPU都有自己的寄存器,它用于存储临时的二进制数据,做一些数据运算。
所以当多个CPU各自运行一个线程的时候,就会导致在寄存器中对数据的修改对其他CPU是不可见的。
然后,一个CPU对变量的写操作都是针对写缓冲器的,并不是直接把值写到主内存中。
所以没有写到主内存中的数据,对其他CPU是不可见的。
然后写入缓冲器后,会把更新后的数据写入到高速缓存中,之后把变量更新信息通过总线通知给其他CPU,但是其他CPU可能会认为这个更新是无效的,不会更新它自己的高速缓存数据,这就导致了高速缓存的可见性问题。
整体的内存模型如图:
MESI协议解决可见性
解决可见性问题的一种方案就是MESI协议,这个MESI协议,根据不同的硬件系统,会有不同的实现方式。
比如MESI的一种实现方式,就是CPU接收到变量更新的消息后,直接更新数据到自己的高速缓存中,这样各个CPU高速缓存中的数据就一致了,解决了可见性问题。
说到MESI协议,王子要跟大家说两个新名词,flush和refresh。
先来说一下flush。
flush就是把自己更新的值刷新到高速缓存(或主内存)中,除了flush操作,同时还会发送一个消息到总线(bus),通知其他处理器某个变量值被修改了。
那refresh又是什么呢?
refresh指的是,处理器中的线程在读取某个变量的时候,如果发现其他处理器的线程修改了这个变量,那就必须过期掉自己高速缓存中的值,从其他处理器的高速缓存(或主内存)中读取变量,同步到自己的高速缓存中。
这就是MESI协议最最基础的原理。
探索有序性问题
之前的文章我们已经说过,指令重排会导致有序性问题,那么具体什么时候会发生指令重排呢?这就要从代码的编译过程说起了。
首先我们写的java代码会被javac静态编译器进行编译,编译成class字节码,然后会经过JIT动态编译器编译成操作系统可以执行的机器码,在编译的过程中,有一个编译优化的概念,为了提高执行效率,可能会发生指令重排,例子就是之前文章中我们说到的double check单例模式,这里就不再说明了。
除了编译会发生指令重排,CPU本身也可能改变指令的执行顺序,另外高速缓存、写缓冲器和无效队列在硬件层面也可能会改变指令的顺序。
接着我们来探索一下CPU是如何出现指令重排的?这就涉及到CPU的指令乱序和猜测执行机制了。
首先我们来看一下指令乱序机制。
CPU获取到的指令是不一定能直接执行的,比如指令要执行网络通信、磁盘IO、获取锁等,为了提升效率,CPU使用的就是指令乱序机制。
把编译好的指令一条一条的读取到处理器中,但哪条指令先就绪可以执行了,就会先执行,而不会去按照顺序执行。
然后将指令执行后的结果放入指令重排序处理器中,重排序处理器再把这些结果按照最开始的指令顺序同步到主内存或写缓冲器中。
这就是指令乱序机制,可能出现有序性问题。
除此之外还有一个猜测执行机制,比如if判断后,执行一堆代码,可能先去执行这堆代码,然后再进行判断,如果判断成立,就采纳执行的结果,否则不采纳执行的结果,这种机制也可能出现有序性问题。
说完了cpu,再来看看高速缓存、写缓冲器是如何导致内存重排序的。
首先来了解两个概念,store和load。
store指的是处理器将数据写入写缓冲器这一过程,load指的是处理器从高速缓存里读数据这一过程。这两个过程可能导致内存重排序,一共有四种可能:
LoadLoad重排序:一个处理器先执行L1,后执行L2,另一个处理器可能看到的是先执行L2,后执行L1;
StoreStore重排序:一个处理器先执行W1,后执行W2,另一个处理器可能看到的是先执行W2,后执行W1;
LoadStore重排序:一个处理器先执行L1,后执行W2,另一个处理器可能看到的是先执行W2,后执行L1;
StoreLoad重排序:一个处理器先执行W1,后执行L2,另一个处理器可能看到的是先执行L2,后执行W1;
为了便于理解,以StoreStore重排序为例,假如高速缓存按照W1W2的顺序接收到两个操作,为了提高性能,先执行了W2,后执行了W1,这就发生了内存重排序,其他处理器看到的顺序就是重新排序后的顺序。
总结
今天我们从硬件级别重新认识了一下可见性和有序性问题。
对于可见性,我们介绍了CPU的缓存机制和MESI协议。
对于有序性,我们介绍了编译优化、CPU的指令乱序和猜测执行、内存重排序机制导致的指令重排问题。
这就是全部内容了,希望小伙伴们能够通过对底层原理的理解,更容易的理解并发编程。
往期文章推荐: