Java中的volatile访问不能被重排序,这是真的吗?

12

注意
当我说一个内存访问可以(或不能)被重新排序时,我指的是它可以相对于任何其他内存访问由编译器在发出字节码时、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=3a=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例如如果没有涉及其他线程。

4个回答

2
JLS中提到(来自JLS-8.3.1.4. volatile Fields),部分内容如下:
Java编程语言提供了第二种机制——volatile字段,对于某些目的而言,它比锁更方便。如果一个字段被声明为volatile,那么Java内存模型确保所有线程都看到该变量的一致值(§17.4)。
这意味着访问可能会被重新排序,但任何重新排序的结果必须与原始顺序在另一个线程中访问时最终保持一致。在单线程应用程序中,字段不需要锁定(来自volatilesynchronization)。

你不能同时拥有临时观察到的重新排序和顺序一致性。请参见我的答案,了解关于重新排序和顺序一致性的允许行为是什么。 - pveentjer

1
Java内存模型为正确同步的程序提供了顺序一致性(SC)。简而言之,SC意味着如果某个程序的所有可能执行方式都可以通过不同的执行方式来解释,其中所有内存操作都按某种顺序执行,并且该顺序与每个线程的程序顺序(PO)一致,则该程序与这些顺序执行一致;因此它是顺序一致的(因此得名)。这实际上意味着JIT/CPU/内存子系统可以随意重新排序易失性写入和读取,只要存在一种顺序执行方式也能解释实际执行结果。因此,实际执行并不重要。如果我们看以下示例:
  volatile int a, b, c;            

  Thread 1                Thread 2

  a = 1;                  while (c != 1); 
  b = 1;                  print(b); 
  c = 1;               

a=1b=2之间有一个发生在之前的关系(PO),以及在c=2c=3之间有一个发生在之前的关系(PO),还有在c=1c!=0之间有一个发生在之前的关系(Volatile variable rule),以及在c!=0print(b)之间有一个发生在之前的关系(PO)。

由于发生在之前的关系是可传递的,因此在a=1print(b)之间也存在一个发生在之前的关系。因此从这个意义上来说,它不能重新排序。但是,没有人能证明已经重新排序了,因此它仍然可以重新排序。


0

我将使用JLS §17.4.5中的符号。

在您的第二个示例中(请原谅我的松散表示法),您有:

线程1排序:
hb(a = 1, b = 2)
hb(b = 2, c = 3)

易失性保证:
hb(b = 2, b != 2)
hb(a = 1, 访问a以进行print)

线程2排序:
hb(while(b != 2);, print(a))

我们有(重点在于“mine”)

更具体地说,如果两个操作之间存在happens-before关系,则它们不一定需要按照那个顺序出现在任何与它们没有共享happens-before关系的代码中。例如,在一个线程中写入与另一个线程中读取发生数据竞争的情况下,这些写入可能会以不同的顺序出现在那些读取中。 c=3和线程2之间不存在happens-before关系。实现可以随心所欲地重新排序c=3

0

来自JLS的17.4内存模型

内存模型描述了程序可能的行为。实现可以生成任何它喜欢的代码,只要程序的所有执行结果都可以由内存模型预测。
这为实现者提供了很大的自由度,可以执行各种代码转换,包括操作的重新排序和不必要同步的删除。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接