天天看点

指令重排序(happens-before + As-If-Serial原则)

指令的基本概念

指令是指示计算机执行某种操作的命令,如:数据传送指令、算术运算指令、位运算指令、程序流程控制指令、串操作指令、处理器控制指令。指令不同于我们所写的代码,一行代码按照操作的逻辑可以分成多条指令。

举个例子:int a = 1; 这段代码大致可以分为两条指令

  1. 加载常量1;
  2. 将常量1赋值给变量a。

指令重排序

Java语言规范JVM线程内部维持顺序花语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。

现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。

指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。

相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段,而两条加法指令在“执行”阶段就只能串行工作。

然而,这样一来,乱序可能就产生了。比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。

一般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。

源代码到最终执行的指令序列示意图

指令重排序(happens-before + As-If-Serial原则)

指令重排序主要分为三种:

  1. 编译器重排序:JVM中进行完成的
  2. 指令级并行重排序
  3. 处理器重排序:CPU中进行完成的

As-If-Serial语义

但是重排序不会造成逻辑混乱吗?比如:

//原代码
a = 1;
a = 2;
if(a == 1){
    a = 3;
}

//乱序后
a = 2;
a = 1;
if(a == 1){
    a = 3;
}
           

这就需要重排序时遵循As-If-Serial语义。对于互不依赖的指令,可以打乱其顺序。

as-if-serial语义的意思是:不管怎么进行指令重排序,单线程内程序的执行结果不能被改变。编译器,处理器进行指令重排序都必须要遵守as-if-serial语义规则。

为了遵守as-if-serial语义,编译器和处理器对存在依赖关系的操作,都不会对其进行重排序,因为这样的重排序很可能会改变执行的结果,但是对不存在依赖关系的操作,就有可能进行重排序。

happens-before规则

虽然As-If-Serial语义可以保证单线程内指令重排序的正确性,但对于多线程还是可能出现问题,多线程环境下存在可见性的问题。

可见性是指当一个线程修改了共享变量的值,其它线程能够适时得知这个修改。在单线程环境中,如果在程序前面修改了某个变量的值,后面的程序一定会读取到那个变量的新值。这看起来很自然,然而当变量的写操作和读操作在不同的线程中时,情况却并非如此。

比如:

class ReorderExample {
int a = 0;
boolean flag = false;

public void writer() {
    a = 1;                   //1
    flag = true;             //2
}

Public void reader() {
    if (flag) {                //3
        int i =  a * a;        //4
        ……
    }
}
}  
           

线程A先执行writer()方法,线程B后执行reader()方法,线程B在执行操作4时,能否看到线程 A 在操作1对共享变量 a 的写入?

答案是不一定,因为存在指令重排序,可能将3、4操作排在1、2之前。正常执行顺序是:1、2、3、4。但是指令重排序后可能变为3、4、1、2。

因此,如果我们需要保证线程安全,就必须保证1、2操作在3、4操作之前,这就需要遵守happens-before规则。

happens-before可以理解为“先于”,是用来指定两个操作之间的执行顺序,由于这个两个操作可以在一个线程之内,也可以在不同线程之间。因此,JMM可以通过happens-before关系来保证跨线程的内存可见性:如果A线程是对变量进行写操作,而B线程是对变量进行读操作,那么如果A线程是先于B线程的操作,那么A线程写操作之后的数据对B线程也是可见的。

happens-before规则:

1.程序次序规则

一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

2.锁定规则

对锁M解锁之前的所有操作Happens-Before对锁M加锁之后的所有操作;

class HappensBeforeLock {
    private int value = 0;
    
    public synchronized void setValue(int value) {
        this.value = value;
    }
    
    public synchronized int getValue() {
        return value;
    }
}
           

上面这段代码,setValue和getValue两个方法共享同一个监视器锁。假设setValue方法在线程A中执行,getValue方法在线程B中执行。setValue方法会先对value变量赋值,然后释放锁。getValue方法会先获取到同一个锁后,再读取value的值。所以根据锁定原则,线程A中对value变量的修改,可以被线程B感知到。

如果这个两个方法上没有synchronized声明,则在线程A中执行setValue方法对value赋值后,线程B中getValue方法返回的value值并不能保证是最新值。

本条锁定规则对显示锁(ReentrantLock)和内置锁(synchronized)在加锁和解锁等操作上有着相同的内存语义。

3.volatile变量规则

对一个变量的写操作先行发生于后面对这个变量的读操作;

4.传递规则

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

5.线程启动规则

Thread对象的start()方法先行发生于此线程的每个一个动作;

6.线程中断规则

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

7.线程终结规则

线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

8.对象终结规则

一个对象的初始化完成先行发生于他的finalize()方法的开始;

指令重排序:

https://www.cnblogs.com/jackeason/p/11336306.html

happens-before:

https://segmentfault.com/a/1190000011458941

https://blog.csdn.net/qq_19642249/article/details/81002210

继续阅读