一、定义
JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
static int i;
static int j;
//某个线程内执行如下赋值操作
j = ....;
i = ....;
可以看到,给i与j的赋值指令先后执行顺序对最终结果不会产生影响,所以代码执行既可以是:
j = ....;
i = ....;
也可以是
i = ....;
j = ....;
注意此种情况是单线程下,两种指令序列等价。上述这种特性称之为【指令重排】,指令重排是JVM自身的优化机制,它不是错误的,但注意多线程下的【指令重排】可能会影响正确性,原因在于指令重排使得指令交错致使执行结果出问题。
指令重排的前提是,重排指令不能影响结果,例如
// 可以重排
int a = 10;// 指令1
int b = 20;// 指令2
System.out.println(a+b);
//不能重排
int a=10; //指令1
int b=a-5;//指令2
二、有序性-指令重排的问题
诡异的结果:
public class Test {
int num=0;
boolean ready =false;
//线程1执行此方法
public void actor1(I_result r){
if(ready){
r.r1=num+num;
}else{
r.r1=1;
}
}
//线程2执行此方法
public void actor2(I_result r){
num=2;
ready=true;
}
}
线程间存在上下文切换,所以可能线程1执行一段代码,再接着线程2执行。
合理可能出现的结果有4、1,但线程2的两条指令可能在JVM的优化下执行指令重排:
public void actor2(I_result r){
ready =true;
num=2;
}
故也可能出现结果为0,这是完全可能出现的结果。
这里也再次注意指令重排的定义:在保证执行结果正确性的前提下,对指令的重新排序,上述例子的actor2()方法虽然指令重排,但是不可否认的是,重排后和重排前对于actor2()来说最后的结果是没有变化的,ready值为true,num值依旧为2。
上述出现结果为0的根源在于多线程指令交错,而不是线程内的指令重排,这点一定要区分清楚。
三、指令重排-禁用
在ready变量上加入volatile,可以保证对ready值改变的那行代码前的所有代码不会重排到此行代码之下,所以上述例子线程2中的两条指令不会被优化重排。简单讲,线程内的代码当涉及访问被volatile修饰的变量的时候,指令重排就会被禁用。
public class TestCurrency {
int num=0;
volatile boolean ready = false;
//线程1执行此方法
public void actor1(I_result r){
if(ready){
r.r1=num+num;
}else{
r.r1=1;
}
}
//线程2执行此方法
public void actor2(I_result r){
num=2;
ready=true;
}
}
四、synchronized与volatile保证有序性的区别
指令重排是指一段代码指令由于JVM本身的优化机制而出现指令重新排列的情况,这种由JVM自身的优化带来的重排序能够保证代码执行结果是正确的,也只有JVM自身允许才会重排列。
- volatile 是细粒度的解决有序性的方式,它能够禁止指令重排,真正做到指令的按序执行。
- synchronized不能禁用其代码块内的指令重排,但是synchronized能保护有序性,其原因是原子性保证了有序性。即不能发生多线程间的指令交错,这从根本上保证了多线程间指令执行的有序性,虽然synchronized内部的指令重排依旧可能出现。
- 有序性和指令重排序是两回事