写点什么

【死磕 Java 并发】-----Java 内存模型之重排序

作者:chenssy
  • 2021 年 11 月 10 日
  • 本文字数:1861 字

    阅读完需:约 6 分钟

【死磕Java并发】-----Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:


  1. 在单线程环境下不能改变程序运行的结果;

  2. 存在数据依赖关系的不允许重排序


如果看过 LZ 上篇博客的就会知道,其实这两点可以归结于一点:无法通过 happens-before 原则推导出来的,JMM 允许任意的排序。

as-if-serial 语义

as-if-serial 语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial 语义。注意 as-if-serial 只保证单线程环境,多线程环境下无效。


下面我们用一个简单的示例来说明:


int a = 1 ;      //Aint b = 2 ;      //Bint c = a + b;   //C
复制代码


A、B、C 三个操作存在如下关系:A、B 不存在数据依赖关系,A 和 C、B 和 C 存在数据依赖关系,因此在进行重排序的时候,A、B 可以随意排序,但是必须位于 C 的前面,执行顺序可以是 A --> B --> C 或者 B --> A --> C。但是无论是何种执行顺序最终的结果 C 总是等于 3。


as-if-serail 语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。


其实对于上段代码,他们存在这样的 happen-before 关系:


  1. A happens-before B

  2. B happens-before C

  3. A happens-before C


1、2 是程序顺序次序规则,3 是传递性。但是,不是说通过重排序,B 可能会排在 A 之前执行么,为何还会存在存在 A happens-beforeB 呢?这里再次申明 A happens-before B 不是 A 一定会在 B 之前执行,而是 A 的对 B 可见,但是相对于这个程序 A 的执行结果不需要对 B 可见,且他们重排序后不会影响结果,所以 JMM 不会认为这种重排序非法。


我们需要明白这点:在不改变程序执行结果的前提下,尽可能提高程序的运行效率。


下面我们在看一段有意思的代码:


public class RecordExample1 {    public static void main(String[] args){        int a = 1;        int b = 2;
try { a = 3; //A b = 1 / 0; //B } catch (Exception e) {
} finally { System.out.println("a = " + a); } }}
复制代码


按照重排序的规则,操作 A 与操作 B 有可能会进行重排序,如果重排序了,B 会抛出异常( / by zero),此时 A 语句一定会执行不到,那么 a 还会等于 3 么?如果按照 as-if-serial 原则它就改变了程序的结果。其实 JVM 对异常做了一种特殊的处理,为了保证 as-if-serial 语义,Java 异常处理机制对重排序做了一种特殊的处理:JIT 在重排序时会在 catch 语句中插入错误代偿代码(a = 3),这样做虽然会导致 cathc 里面的逻辑变得复杂,但是 JIT 优化原则是:尽可能地优化程序正常运行下的逻辑,哪怕以 catch 块逻辑变得复杂为代价。

重排序对多线程的影响

在单线程环境下由于 as-if-serial 语义,重排序无法影响最终的结果,但是对于多线程环境呢?


如下代码(volatile 的经典用法):


public class RecordExample2 {    int a = 0;    boolean flag = false;
/** * A线程执行 */ public void writer(){ a = 1; // 1 flag = true; // 2 }
/** * B线程执行 */ public void read(){ if(flag){ // 3 int i = a + a; // 4 } }
}
复制代码


A 线程执行 writer(),线程 B 执行 read(),线程 B 在执行时能否读到 a = 1 呢?答案是不一定(注:X86CPU 不支持写写重排序,如果是在 x86 上面操作,这个一定会是 a=1,LZ 搞了好久都没有测试出来,最后查资料才发现)。


由于操作 1 和操作 2 之间没有数据依赖性,所以可以进行重排序处理,操作 3 和操作 4 之间也没有数据依赖性,他们亦可以进行重排序,但是操作 3 和操作 4 之间存在控制依赖性。假如操作 1 和操作 2 之间重排序:



按照这种执行顺序线程 B 肯定读不到线程 A 设置的 a 值,在这里多线程的语义就已经被重排序破坏了。


操作 3 和操作 4 之间也可以重排序,这里就不阐述了。但是他们之间存在一个控制依赖的关系,因为只有操作 3 成立操作 4 才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响。假如操作 3 和操作 4 重排序了,操作 4 先执行,则先会把计算结果临时保存到重排序缓冲中,当操作 3 为真时才会将计算结果写入变量 i 中


通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

参考资料

  1. 周志明 :《深入理解 Java 虚拟机》

  2. 方腾飞:《Java 并发编程的艺术》

发布于: 33 分钟前阅读数: 6
用户头像

chenssy

关注

『Java 技术驿站』、死磕 Java 系列 2018.03.24 加入

全职 Java 程序员,兼职写文章,专注【死磕 Java】系列文章创作,一个立志做专业的程序员...

评论

发布
暂无评论
【死磕Java并发】-----Java内存模型之重排序