注意
当我说一个内存访问可以(或不能)被重新排序时,我指的是它可以相对于任何其他内存访问由编译器在发出字节码时、JIT在发出机器代码时或CPU在乱序执行时(可能需要屏障来防止这种情况)进行重新排序。
经常听说访问volatile
变量由于Happens-Before关系(HBR)而不能被重新排序。
我发现给定线程的每两个连续操作之间都存在HBR,但它们仍然可以被重新排序。
此外,只有与同一变量/字段上的访问相关的volatile
访问才会HB。
我认为使volatile
不可重新排序的原因是:
对一个
volatile
字段(§8.3.1.4)的写入Happens-Before该字段的每次后续读取[任何线程]。
如果存在其他线程,那么变量的重新排序将变得可见,就像在这个简单的例子中一样。
volatile int a, b;
Thread 1 Thread 2
a = 1; while (b != 2);
b = 2; print(a); //a must be 1
因此,防止排序的不是HBR本身,而是volatile
将此关系与其他线程扩展,其他线程的存在是防止重新排序的元素。
如果编译器能够证明对易失变量的重新排序不会改变程序语义,它可以重新排序即使存在HBR。
如果易失变量从未被其他线程访问,则其访问可以被重新排序。
volatile int a, b, c;
Thread 1 Thread 2
a = 1; while (b != 2);
b = 2; print(a); //a must be 1
c = 3; //c never accessed by Thread 2
我认为c=3
在a=1
之前重新排列是完全可以的,规范中的这句话证实了这一点。
应注意,在两个动作之间存在happens-before关系并不一定意味着它们必须按照特定顺序在实现中执行。 如果重新排序可以产生与合法执行一致的结果,则不违反规则。
因此,我制作了这些简单的Java程序。
public class vtest1 {
public static volatile int DO_ACTION, CHOOSE_ACTION;
public static void main(String[] args) {
CHOOSE_ACTION = 34;
DO_ACTION = 1;
}
}
public class vtest2 {
public static volatile int DO_ACTION, CHOOSE_ACTION;
public static void main(String[] args) {
(new Thread(){
public void run() {
while (DO_ACTION != 1);
System.out.println(CHOOSE_ACTION);
}
}).start();
CHOOSE_ACTION = 34;
DO_ACTION = 1;
}
}
在这两种情况下,这两个字段都被标记为volatile
并且使用putstatic
进行访问。
由于这是JIT所拥有的所有信息1,机器代码将是相同的,
因此,vtest1
访问不会被优化2。
我的问题
规范上真的永远不会重新排序volatile访问或者他们可能会被重新排序3,但实际上从未这样做吗?
如果volatile访问永远不会被重新排序,规范的哪些部分说明了这一点?这是否意味着CPU会按程序顺序执行并查看所有volatile访问?
1或者JIT能知道其他线程永远不会访问该字段吗?如果可以,如何实现?。
2例如存在内存屏障。
3例如如果没有涉及其他线程。