三、Java内存模型之重排序

2019-07-27  本文已影响0人  liyf155

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,但需要满足以下两个条件:

  1. 单线程环境下不能改变程序运行的结构;
  2. 存在数据依赖关系的不允许重排序;

as-if-serial语义

as-if-serital意思是,所有操作均可以为了优化而被重排序,但是必须保证重排序后执行的结果不能被改变,编译器、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下是无效的。

代码示例

double pi = 3.14;         // A
double r = 1.0;           // B
double area = pi * r * r  // C

A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A和C、B和C存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于C的前面,执行顺序可以是A –> B –> C或者B –> A –> C。但是无论是何种执行顺序最终的结果是不会变的。
as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。

其实对于上段代码,他们存在这样的happen-before关系:

  1. A happens-before B
  2. B happens-before C
  3. A happens-before C

1、2是程序顺序次序规则,3是传递性。但是,不是说通过重排序,B可能会排在A之前执行么,为何还会存在存在A happens-beforeB呢?这里再次申明A happens-before B不是A一定会在B之前执行,而是A的对B可见,但是相对于这个程序A的执行结果不需要对B可见,且他们重排序后不会影响结果,所以JMM不会认为这种重排序非法。

我们需要明白这点:在不改变程序执行结果的前提下,尽可能提高程序的运行效率。

再来看看下面一段代码:

/**
 * 重排序示例
 */
public class RecordExample {
    public static void main(String[] args) {
        // a 和 b 没有任何依赖关系,处理器和编译器可能会进行重排序
        int a = 1;
        int b = 2;

        try {
            // 将a赋值为3
            a = 3;
            // b除以0,运行时异常
            b = b / 0;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // a 等于什么???
            System.out.println("a = " + a);
        }
    }
}

按照重排序的规则,操作A与操作B有可能会进行重排序,如果重排序了,B会抛出异常( / by zero),此时A语句一定会执行不到,那么a还会等于3么?如果按照as-if-serial原则它就改变了程序的结果。其实JVM对异常做了一种特殊的处理,为了保证as-if-serial语义,Java异常处理机制对重排序做了一种特殊的处理:JIT在重排序时会在catch语句中插入错误代偿代码(a = 3),这样做虽然会导致cathc里面的逻辑变得复杂,但是JIT优化原则是:尽可能地优化程序正常运行下的逻辑,哪怕以catch块逻辑变得复杂为代价。

重排序对多线程的影响

在单线程环境下由于as-if-serial语义,重排序无法影响最终的结果,但是对于多线程环境呢?

如下代码(volatile的经典用法):

public class RecordExample2 {
    static int a = 0;
    static boolean flag = false;

    /**
     * A线程执行
     */
    public static void writer() {
        // ①
        a = 1;
        // ②
        flag = true;
        System.out.println("a = " + a + ", flag = " + flag);
    }

    /**
     * B线程执行
     */
    public static void read() {
        // ③
        if (flag) {
            // ④
            int i = a + a;
            System.out.println("i = " + i);
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                writer();
            }
        });
        t1.start();
        Thread.sleep(3000);

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });
        t2.start();
    }
}

A线程执行writer(),线程B执行read(),线程B在执行时能否读到 a = 1 呢?答案是不一定。

由于操作1 和操作2 之间没有数据依赖性,所以可以进行重排序处理,操作3 和操作4 之间也没有数据依赖性,他们亦可以进行重排序,但是操作3 和操作4 之间存在控制依赖性。


image-20190724175815737.png

按照这种执行顺序线程B肯定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。

操作3 和操作4 之间也可以重排序,这里就不阐述了。但是他们之间存在一个控制依赖的关系,因为只有操作3 成立操作4 才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响。假如操作3 和操作4重排序了,操作4 先执行,则先会把计算结果临时保存到重排序缓冲中,当操作3 为真时才会将计算结果写入变量i中

通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

上一篇 下一篇

猜你喜欢

热点阅读