指令重排有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一般分为以下三种:

image.png

单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。
处理器在进行指令重排序时必须考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。

指令重排序案例分析

  1. public class BanCommandReSortSeq {
  2. int a = 0;
  3. boolean flag = false;
  4. public void methodOne() {
  5. a = 1;
  6. // 语句1
  7. flag = true;
  8. // 语句2
  9. // methodOne发生指令重排,程序执行顺序可能如下:
  10. // flag = true;
  11. // 语句2
  12. // a = 1;
  13. // 语句1
  14. }
  15. public void methodTwo() {
  16. if (flag) {
  17. a = a + 5;
  18. // 语句3
  19. }
  20. System.out.println("methodTwo ret a = " + a);
  21. }
  22. }

多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。
多线程交替调用会出现如下场景:
线程1调用methodOne,如果此时编译器进行指令重排
methodOne代码执行顺序变为:语句2(flag=true) -> 语句1(a=5)
线程2调用methodTwo,由于flag=true,如果此时语句1还没有执行(语句2 -> 语句3 -> 语句1 ),那么执行语句3的时候a的初始值=0
所以最终a的返回结果可能为 a = 0 + 5 = 5,而不是我们认为的a = 1 + 5 = 6;

volition如何防止指令重排序


在每个volatile读操作的后面插入两个屏障:LoadLoad 和loadstore屏障。

loadload屏障:对于这样的语句load1; loadload; load2,在load2及后续读取操作要读取的数据被访问前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadload屏障,那么load1指令一定会在load2之前执行,CPU不会对load1与load2进行重排序)

loadstore屏障:对于这样的语句load1; loadstore; store2,在store2及后续写入操作被刷出前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadstore屏障,那么load1指令一定会在store2之前执行,CPU不会对load1与store2进行重排序)

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作执行的顺序性;
  • 保证某些变量的内存可见性(利用该特性实现volatile内存可见性)

volatile实现禁止指令重排优化底层原理:

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排,也就是说通过插入内存屏障,就能禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

左边:写操作场景:先LoadStore指令,后LoadLoad指令。
右边:读操作场景:先LoadLoad指令,后LoadStore指令。

volatile写:
image.png

volatile读:
image.png