一、定义

JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

  1. static int i;
  2. static int j;
  3. //某个线程内执行如下赋值操作
  4. j = ....;
  5. i = ....;

可以看到,给i与j的赋值指令先后执行顺序对最终结果不会产生影响,所以代码执行既可以是:

  1. j = ....;
  2. i = ....;

也可以是

  1. i = ....;
  2. j = ....;

注意此种情况是单线程下,两种指令序列等价。上述这种特性称之为【指令重排】,指令重排是JVM自身的优化机制,它不是错误的,但注意多线程下的【指令重排】可能会影响正确性,原因在于指令重排使得指令交错致使执行结果出问题。

指令重排的前提是,重排指令不能影响结果,例如

  1. // 可以重排
  2. int a = 10;// 指令1
  3. int b = 20;// 指令2
  4. System.out.println(a+b);
  5. //不能重排
  6. int a=10; //指令1
  7. int b=a-5;//指令2


二、有序性-指令重排的问题

诡异的结果:

  1. public class Test {
  2. int num=0;
  3. boolean ready =false;
  4. //线程1执行此方法
  5. public void actor1(I_result r){
  6. if(ready){
  7. r.r1=num+num;
  8. }else{
  9. r.r1=1;
  10. }
  11. }
  12. //线程2执行此方法
  13. public void actor2(I_result r){
  14. num=2;
  15. ready=true;
  16. }
  17. }

线程间存在上下文切换,所以可能线程1执行一段代码,再接着线程2执行。
合理可能出现的结果有4、1,但线程2的两条指令可能在JVM的优化下执行指令重排:

  1. public void actor2(I_result r){
  2. ready =true;
  3. num=2;
  4. }

故也可能出现结果为0,这是完全可能出现的结果。

这里也再次注意指令重排的定义:在保证执行结果正确性的前提下,对指令的重新排序,上述例子的actor2()方法虽然指令重排,但是不可否认的是,重排后和重排前对于actor2()来说最后的结果是没有变化的,ready值为true,num值依旧为2。
上述出现结果为0的根源在于多线程指令交错,而不是线程内的指令重排,这点一定要区分清楚。

三、指令重排-禁用

在ready变量上加入volatile,可以保证对ready值改变的那行代码前的所有代码不会重排到此行代码之下,所以上述例子线程2中的两条指令不会被优化重排。简单讲,线程内的代码当涉及访问被volatile修饰的变量的时候,指令重排就会被禁用。

  1. public class TestCurrency {
  2. int num=0;
  3. volatile boolean ready = false;
  4. //线程1执行此方法
  5. public void actor1(I_result r){
  6. if(ready){
  7. r.r1=num+num;
  8. }else{
  9. r.r1=1;
  10. }
  11. }
  12. //线程2执行此方法
  13. public void actor2(I_result r){
  14. num=2;
  15. ready=true;
  16. }
  17. }


四、synchronized与volatile保证有序性的区别

指令重排是指一段代码指令由于JVM本身的优化机制而出现指令重新排列的情况,这种由JVM自身的优化带来的重排序能够保证代码执行结果是正确的,也只有JVM自身允许才会重排列。

  • volatile 是细粒度的解决有序性的方式,它能够禁止指令重排,真正做到指令的按序执行。
  • synchronized不能禁用其代码块内的指令重排,但是synchronized能保护有序性,其原因是原子性保证了有序性。即不能发生多线程间的指令交错,这从根本上保证了多线程间指令执行的有序性,虽然synchronized内部的指令重排依旧可能出现。
  • 有序性和指令重排序是两回事