- Java并发编程深度解析与实战
- 谭锋(Mic)
- 1776字
- 2022-05-10 18:39:21
3.4 指令重排序导致的可见性问题
什么是指令重排序呢?为了更加直观地理解,笔者还是通过一个案例来说明。
上面这段程序的逻辑如下:
• 定义四个int类型的变量,初始化都为0。
• 定义两个线程t1、t2,t1线程修改a和x的值,t2线程修改b和y的值,分别启动两个线程。
• 正常情况下,x和y的值,会根据t1和t2线程的执行情况来决定。
○ 如果t1线程优先执行,那么得到的结果是x=0、y=1。
○ 如果t2线程优先执行,那么得到的结果是x=1、y=0。
○ 如果t1和t2线程同时执行,那么得到的结果是x=1、y=1。
我们来看一下运行结果:
读者看到这个结果是不是大吃一惊?在运行了13万次之后,竟然得到一个x=0、y=0的结果。
其实这就是所谓的指令重排序问题,假设上面的代码通过指令重排序之后,变成下面这种结构:
经过重排序之后,如果t1和t2线程同时运行,就会得到x=0、y=0的结果,这个结果从人的视角来看,就有点类似于t1线程中a=1的修改结果对t2线程不可见,同样t2线程中b=1的执行结果对t1线程不可见。
3.4.1 什么是指令重排序
指令重排序是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。
从源代码到最终运行的指令,会经过如下两个阶段的重排序。
第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。在前面分析JIT优化中提到的循环表达式外提(Loop Expression Hoisting)就是编译器层面的重排序,从CPU层面来说,避免了处理器每次都去内存中加载stop,减少了处理器和内存的交互开销。
第二阶段,处理器重排序,处理器重排序分为两个部分。
• 并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。
• 内存系统重排序,这是处理器引入Store Buffer缓冲区延时写入产生的指令执行顺序不一致的问题,在后续内容中会详细说明。
为了帮助读者理解,笔者专门针对并行指令集的原理做一个简单的说明。
什么是并行指令集?在处理器内核中一般会有多个执行单元,比如算术逻辑单元、位移单元等。在引入并行指令集之前,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。
那么什么是并行指令集的重排序呢?如图3-12所示,假设某一段程序有多条指令,不同指令的执行实现也不同。对于一条从内存中读取数据的指令,CPU的某个执行单元在执行这条指令并等到返回结果之前,按照CPU的执行速度来说它足够处理几百条其他指令,而CPU为了提高执行效率,会根据单元电路的空闲状态和指令能否提前执行的情况进行分析,把那些指令地址顺序靠后的指令提前到读取内存指令之前完成。
实际上,这种优化的本质是通过提前执行其他可执行指令来填补CPU的时间空隙,然后在结束时重新排序运算结果,从而实现指令顺序执行的运行结果。
图3-12 并行指令集重排序
3.4.2 as-if-serial语义
as-if-serial表示所有的程序指令都可以因为优化而被重排序,但是在优化的过程中必须要保证是在单线程环境下,重排序之后的运行结果和程序代码本身预期的执行结果一致,Java编译器、CPU指令重排序都需要保证在单线程环境下的as-if-serial语义是正确的。
可能有些读者会有疑惑,既然能够保证在单线程环境下的顺序性,那为什么还会存在指令重排序呢?在JSR-133规范中,原文是这么说的。
The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings. However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.
as-if-serial语义允许重排序,CPU层面的指令优化依然存在。在单线程中,这些优化并不会影响整体的执行结果,在多线程中,重排序会带来可见性问题。
另外,为了保证as-if-serial语义是正确的,编译器和处理器不会对存在依赖关系的操作进行指令重排序,因为这样会影响程序的执行结果。我们来看下面这段代码。
上述代码按照正常的执行顺序应该是1、2、3,在多线程环境下,可能会出现2、1、3这样的执行顺序,但是一定不会出现3、2、1这样的顺序,因为3与1和2存在数据依赖关系,一旦重排序,就无法保证as-if-serial语义是正确的。
至此,相信读者对指令重排序导致的可见性问题有了一个基本的了解,但是在CPU层面还存在内存系统重排序问题,内存系统重排序也会导致可见性问题,下面笔者围绕这个问题做一个详细的分析。