Java并发拾遗(二)——重排序

一、 as-if-serial语义

上篇文章中说道,编译器,运行时的JIT编译器或处理器都会对指令进行重排序以提升程序的执行性能。但这些重排序需要满足as-if-serial语义,不能随便的进行重排序。as-if-serial语义即指:不管怎么重排序,单线程程序的执行结果不能被改变。因此,不能对存在数据依赖关系的操作进行重排序。举个栗子:

    double pi = 3.14;
    double r = 1.0;
    double area = pi * r * r;  

在上面的三个操作中,area变量依赖于 pi 与 r,因此在进行重排序时,pi 与 r 哪个操作先被执行是不确定的,可被重排序的,但是area就不能被重排序,其必须等到pi 与 r的操作完成才能开始执行。这种执行顺序是由编译器、runtime与处理器的重排序机制来保证的,我们可以放心的依赖这种规则。但是,请注意,说这么一大堆的前提是单线程程序,多线程引起之间的顺序错乱是并发引起的,而不是重排序。

上篇文章中,说道Java内置的happens-before规则有一条说:单线程程序,前面的代码 happens-before 后面的代码。但这种说法是说对存在依赖关系的两个操作来讲的,对不存在依赖关系的操作(如两个操作操作不同变量),Java是允许其进行重排序的,因为这可以使程序执行的更快。

二、重排序对多线程的影响

上面所说内容,都是在单线程的前提下进行阐述的,下面来看重排序对多线程的影响。看个栗子:

    private class Test {
        int a = 0;
        boolean flag = false;

        public void write() {
            a = 1;
            flag = true;
        }

        public int read() {
            if(flag) {
                int i = a;
            }
            
            // ** 
        }
    }

如上的代码,flag的初衷是个标记量,用来表示变量a是否被写入了,a写入后才更改flag。然而在多线程的环境下,这种初衷就可能被重排序破坏,从而无法得到保证。这种破坏,提现在两处:

1. 在write()方法内对不存在依赖关系的两条指令重排序

由于在write()方法内的两条操作不存在依赖关系,因而可能被重排序,导致如下图的执行顺序

即标志位flag先被置为TRUE,而a还没被赋值,然后线程B就开始执行了。

2. 对read()方法内存在控制依赖性的语句进行猜测执行导致的重排序

为了提升并行度,加快执行,在实际的执行过程中,编译器或处理器在执行read()方法时可能采取猜测执行的策略来执行。即先把a读进来,然后再判断flag,flag = true时就把预读的a赋值给i,flag = false时就丢弃a。这种对控制依赖性的猜测执行,实际上对read()方法内的语句进行了重排序。

从以上两点,可知在多线程环境下,指令的重排序对于最终的程序结果会产生影响,因而在多线程并发的环境下,需要对重排序进行控制,在一些会影响结果的地方,禁止重排序。

posted @ 2017-04-17 23:19  Mr.do  阅读(329)  评论(0编辑  收藏  举报