指令重排序
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;
}
}
代码执行后结果如下:
结果确实如我们想的那样,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()方法的开始;
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!