🌈🌈🌈🌈🌈🌈🌈🌈
欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
🍁🍁🍁🍁🍁🍁🍁🍁
这里为什么要了解一下可见性的底层原理呢?
因为对于可见性这块的内容,他并不是软件层面上的问题,而是硬件层面的问题,是底层的一些机制导致了可见性的问题,了解了底层的相关内容之后,我们的知识会更容易形成一个闭环,而不仅仅是停留于软件层面,对下层一无所知!
所以接下来聊一聊底层中到底是什么原因导致了不同线程之间出现这个可见性的问题
首先,每一个处理器都有自己的寄存器,而线程对变量的读操作都是针对写缓冲进行的,因此这个可见性问题与 寄存器
和 写缓冲
这两个硬件组件是有关联的
这里分别说一下寄存器和写缓冲 如何导致了可见性的问题
:
多个处理器都在运行各自的线程的时候,如果其中一个处理器中的线程将某一个变量更新后的值放在寄存器中
,那么其他处理器中的线程是没有办法看到这个更新后的值的,因为这个寄存器是各个处理器私有的,因此,寄存器会导致可见性的问题
处理器运行的线程,对变量的写操作是针对写缓冲进行的
,之后才会刷到主内存中,因此如果一个线程更新了变量,如果仅仅写入到了写缓冲充,还没有刷到主内存或高速缓存中,那么其他处理器中的线程是无法感知到这个变量的修改的,此时,导致可见性的问题
即使这个写缓冲的数据的更新也同步到了自己的主内存或高速缓存里,并且将这个更新通知给了其他的处理器,但是其他处理器可能把这个更新放到无效队列中,并没有更新自己的高速缓存,此时仍然会导致可见性的问题
如下这个图:
那么要实现多个处理器的共享数据的一致性,可以通过 MESI 协议来实现
根据具体底层硬件的不同,MESI 协议的具体实现也是不同的
这里说一种 MESI 协议的实现:通过将其他处理器高速缓存中 更新后的数据
拿到自己的高速缓存中更新一下,这样不同处理器之间的高速缓存中的数据就保持一致了,实现了可见性
在实现 MESI 协议的过程中,需要 两个关键的机制
来确保缓存的一致性:flush 和 refresh
将自己更新的值刷新到高速缓存里去,让其他处理器在后续可以通过一些机制从自己的高速缓存里读到更新后的值
并且还会给其他处理器发送一个 flush 消息,让其他处理器将对应的缓存行标记为无效,确保其他处理器不会读到这个变量的过时版本
处理器中的线程在读取一个变量的值的时候,如果发现其他处理器的线程更新了变量的值,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的值,更新到自己的高速缓存中
因此,在底层通过 MESI 协议、flush 处理器缓存和 refresh 处理器缓存来保证可见性的
总结一下就是,flush 是强制将更新后的数据从写缓冲器中刷新到高速缓存中去;refresh 是去感知到其他处理器更新了变量,主动从主内存或其他处理器的高速缓存中加载最新数据
那么举个例子,对于 volatile
变量来说:
volatile boolean flag = true;
当写 volatile 变量时,就会通过执行一个内存屏障,在底层会触发flush处理器缓存的操作,把数据刷到主内存中
当读 volatile 变量时,也会通过执行一个内存屏障,在底层触发refresh操作,从主内存中,读取最新的值
指令重排的内容我们可以来了解一下,什么时候会发生指令重排
指令重排指的是我们写好的代码,在真正执行的时候,执行顺序可能会被重排序,如果重排序之后,在多线程的执行环境下,可能就会出现一些问题
什么时候会发生指令重排呢?
Java 中有两种编译器,一种是静态编译器(javac),另一种是动态编译器(JIT)
javac 负责把 .java 文件中的源代码编译为 .class 文件中的字节码,这个一般是程序写好之后进行编译的
JIT 是 JVM 的一部分,负责把 .class 文件中的字节码编译为 JVM 所在操作系统支持的机器码,一般在程序运行过程中进行编译
那么在编译期间,可能编译器为了提高代码的执行效率,会对指令进行重排,JIT 对指令重排还是比较多的
编译器编译好的指令,到真正处理器执行的时候,可能还会调整顺序
指令重排有什么规则约束呢?
上边讲过了一个 happens-before 原则,它定义了一些规则,只要符合 happens-before 中的规则的都不会进行指令重排
就比如说,下边代码的第三行不可能重排到上边,因为它的执行结果依赖了上边两行的执行结果,因此不会重排,但是前两行可能会重排:
int a = 1;
int b = 2;
int c = a + b;