写点什么

指令重排序导致的可见性问题

  • 2021 年 11 月 18 日
  • 本文字数:2016 字

    阅读完需:约 7 分钟

01

什么是指令重排序

指令重排序是指编译器或 CPU 为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。

从源代码到最终运行的指令,会经过如下两个阶段的重排序。

第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少 CPU 和内存的交互,重排序之后尽可能保证 CPU 从寄存器或缓存行中读取数据。

在前面分析 JIT 优化中提到的循环表达式外提(Loop Expression Hoisting)就是编译器层面的重排序,从 CPU 层面来说,避免了处理器每次都去内存中加载 stop,减少了处理器和内存的交互开销。

if(!stop){    while(true){        i++;    }}
复制代码

第二阶段,处理器重排序,处理器重排序分为两个部分。

  • 并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。

  • 内存系统重排序,这是处理器引入 Store Buffer 缓冲区延时写入产生的指令执行顺序不一致的问题,在后续内容中会详细说明。

为了帮助读者理解,笔者专门针对并行指令集的原理做一个简单的说明。

什么是并行指令集?

在处理器内核中一般会有多个执行单元,比如算术逻辑单元、位移单元等。在引入并行指令集之前,CPU 在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;在引入并行指令集之后,CPU 在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。

那么什么是并行指令集的重排序呢?

如下图所示,假设某一段程序有多条指令,不同指令的执行实现也不同。



图  并行指令集重排序

对于一条从内存中读取数据的指令,CPU 的某个执行单元在执行这条指令并等到返回结果之前,按照 CPU 的执行速度来说它足够处理几百条其他指令,而 CPU 为了提高执行效率,会根据单元电路的空闲状态和指令能否提前执行的情况进行分析,把那些指令地址顺序靠后的指令提前到读取内存指令之前完成。

实际上,这种优化的本质是通过提前执行其他可执行指令来填补 CPU 的时间空隙,然后在结束时重新排序运算结果,从而实现指令顺序执行的运行结果。


02

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 语义是正确的,编译器和处理器不会对存在依赖关系的操作进行指令重排序,因为这样会影响程序的执行结果。我们来看下面这段代码。

public void execute(){    int x=10;  //1    int y=5;   //2    int c=x+y; //3}
复制代码

上述代码按照正常的执行顺序应该是 1、2、3,在多线程环境下,可能会出现 2、1、3 这样的执行顺序,但是一定不会出现 3、2、1 这样的顺序,因为 3 与 1 和 2 存在数据依赖关系,一旦重排序,就无法保证 as-if-serial 语义是正确的。

至此,相信读者对指令重排序导致的可见性问题有了一个基本的了解,但是在 CPU 层面还存在内存系统重排序问题,内存系统重排序也会导致可见性问题,《Java 并发编程深度解析与实战》一书还会围绕这个问题做一个详细的分析。

*本文节选自《Java 并发编程深度解析与实战》一书,欢迎阅读本书了解更多精彩内容!


java并发编程9787121421365.jpg


▊《Java 并发编程深度解析与实战》

谭锋 著


  • Java 并发编程集大成之作

  • 13 年 Java 开发及架构经验+4 年对并发编程深度研究总结

  • 涵盖 Java 整个并发编程体系的核心库和核心类

  • 大量的设计思想与实际案例结合


本书涵盖 Java 并发编程体系的核心库和核心类的使用及原理分析,具体包括线程、synchronized、volatile、J.U.C 中的重入锁和读写锁、并发中的条件等待机制、J.U.C 并发工具集、深度探索并发编程不得不知的工具、阻塞队列、并发安全集合、线程池、异步编程特性等。书中针对每一个技术点,纵向分析与其相关的所有内容,并且对相关知识点进行了非常详细的说明,同时从架构实践的角度来看待并发,通过大量实战案例让读者理解各类技术在实际应用中的使用方法。

用户头像

还未添加个人签名 2019.10.21 加入

还未添加个人简介

评论

发布
暂无评论
指令重排序导致的可见性问题