指令重排序

catch return finally还会执行吗

之前面试的时候面试官问了这么一个问题:catch return finally还会执行吗。
当时想的就是:啊!还可以这么写,从来没想过这种问题,但是想到finally一开始学的时候就说是一定是会执行的。基于这个,还是心虚的回答了:会执行,应该是先执行finally,再return。当时不知道是什么原理,后来想起来,决定整理个这个问题,做个记录。

首先我们来用一段代码模拟下这个场景

package org.example;

public class Test {
    public static void main(String[] args) {
        test();
    }

    private static int test() {
        int a = 1;
        try {
            a = 3/0;
        } catch (Exception e) {
            System.out.println("error");
            return a;
        } finally {
            System.out.println("finally");
        }
        return a;
    }
}

代码执行后结果如下:
指令重排序_第1张图片
结果确实如我们想的那样,finally确实执行了,return也成功返回了,这是什么原因呢?我们debug跟踪下,发现return语句运行了两次,但是第一次并没有return。考虑到源码和class文件的区别,我们去看下class文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.example;

public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        test();
    }

    private static int test() {
        int a = 1;

        byte var2;
        try {
            a = 3 / 0;
            return a;
        } catch (Exception var6) {
            System.out.println("error");
            var2 = (byte)a;
        } finally {
            System.out.println("finally");
        }

        return var2;
    }
}

通过观察class我们可以发现,return语句的位置变了,移到了catch中,但是finally确实是最后执行,这种改变语句顺序但不影响最终结果的就是指令重排序

指令重排序

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

源代码到最终执行的指令序列示意图
在这里插入图片描述

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

  • 编译器重排序:JVM中进行完成的

  • 指令级并行重排序

  • 处理器重排序:CPU中进行完成的

As-If-Serial语义

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

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

这就需要重排序时遵循As-If-Serial语义。对于互不依赖的指令,可以打乱其顺序。存在依赖关系的操作,都不会对其进行重排序,因为这样的重排序很可能会改变执行的结果。

happens-before规则

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

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

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

happens-before规则:

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

  • 锁定规则
    对锁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)在加锁和解锁等操作上有着相同的内存语义。

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

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

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

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

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

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

你可能感兴趣的:(jvm,java,jvm)