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()方法内的语句进行了重排序。
从以上两点,可知在多线程环境下,指令的重排序对于最终的程序结果会产生影响,因而在多线程并发的环境下,需要对重排序进行控制,在一些会影响结果的地方,禁止重排序。