读取的重新排序

2022-09-02 01:05:45

假设有两个线程没有同步,一个设置另一个执行 。n = 1method()

在下面,“读取”始终是指对字段的读取。n

public class MyClass
{
  public int n = 0;

  public void method() {
    System.out.println(n); //read 1
    System.out.println(n); //read 2
  }
}

以下输出是否可行?

1
0

答案是肯定的,因为即使读取 1 发生在读取 2 之前,读取 2 也可能在读取 1 之前重新排序,因为它不会更改线程内执行的语义。

这个推理正确吗?


答案 1

发生在之前并不意味着两个任意操作的顺序。更准确地说,在发生之前发生的最重要的事情是将写入读取捆绑在发生之前 - 一致性。值得注意的是,它告诉了读取可以观察到的写入:最后一次写入按发生顺序进行(在发生之前)或任何其他未按发生顺序排列的写入(种族)。请注意,两次连续读取可能会看到从不同的(不规则)写入中获得的不同值,而不会违反该要求。

例如,JLS 17.4.5 说:

应该注意的是,两个操作之间存在“发生之前”关系并不一定意味着它们在实现中必须按该顺序发生。如果重新排序产生与合法执行一致的结果,则不违法。

数据竞赛是令人毛骨悚然的:不规则的读取可以在每次读取时返回令人惊讶的数据,而Java内存模型可以捕获这一点。因此,更准确的答案是,生成 (1, 0) 的执行不违反 Java 内存模型约束(同步顺序一致性、同步顺序 - 程序顺序一致性、发生前一致性、因果关系要求),因此是允许的。

实现方面:在硬件上,两个负载都可以在不同的时间启动和/或到达内存子系统,无论它们的“程序顺序”如何,因为它们是独立的;在编译器中,指令调度也可能忽略独立读取的程序顺序,以“反直觉”顺序向硬件公开负载。

如果要按程序顺序观察读取,则需要更强的属性。JMM 将该属性赋予同步操作(在您的示例中,创建一个变量将创建该属性),这会将操作按与程序顺序一致的同步顺序绑定。在这种情况下,(1,0)将被禁止。volatile

个非常特殊的jcstress测试用例的插图(参见完整的源代码的警告):

private final Holder h1 = new Holder();
private final Holder h2 = h1;

private static class Holder {
    int a;
    int trap;
}

@Actor
public void actor1() {
    h1.a = 1;
}

@Actor
public void actor2(IntResult2 r) {
    Holder h1 = this.h1;
    Holder h2 = this.h2;
    h1.trap = 0;
    h2.trap = 0;
    r.r1 = h1.a;
    r.r2 = h2.a;
}

即使在不重新排序加载的 x86 上,也会产生 (1, 0),哎呀:

      [OK] o.o.j.t.volatiles.ReadAfterReadTest                                                                                                      
    (fork: #1, iteration #1, JVM args: [-server])
  Observed state   Occurrences              Expectation  Interpretation                                              
          [0, 0]    16,736,450               ACCEPTABLE  Doing both reads early.                                     
          [1, 1]   108,816,262               ACCEPTABLE  Doing both reads late.                                      
          [0, 1]         3,941               ACCEPTABLE  Doing first read early, not surprising.                     
          [1, 0]        84,477   ACCEPTABLE_INTERESTING  First read seen racy value early, and the s...

使挥发性会使(1,0)消失。Holder.a


答案 2

我们有 4 个操作,它们形成以下发生前图:

+-------+     ?    +-------+
| n = 0 |   ---->  | n = 1 |
+-------+          +-------+
    |
    |?
    v
  +---+             +---+
  | n |     ---->   | n |
  +---+             +---+

由于您没有给出初始化 n 的代码,因此不知道 n=0 是否在 n=1 之前发生,以及 n=0 是否在第一次读取 n 之前发生。

如果这些边不存在,(n=1, n, n=0, n)是顺序一致的执行顺序,输出 1 0 是平凡可能的。

如果已知 n=0 发生在 n=1 之前,则与输出 1 0 没有顺序一致的执行。

但是,Java语言规范仅保证所有执行在没有数据竞争的情况下是顺序一致的,而我们的程序不是这样。具体来说,该规范写道:

更具体地说,如果两个操作共享“发生前”关系,则对于它们不共享“发生前发生”关系的任何代码,它们不一定必须显示为已发生。例如,一个线程中的写入与另一个线程中的读取在一个线程中,对于这些读取来说,似乎发生的顺序不正确。

我们说,变量 v 的读取 r 允许观察写入 w 到 v,如果在执行跟踪的 happen-before 偏序中:

  • r 在 w 之前没有排序(即 hb(r, w) 不是这种情况),并且

  • 没有干预写w'到v'(即没有写w'到v,使得hb(w,w')和hb(w',r))。

在我们的例子中,两个读取都允许同时观察0和1,因为没有干预写入。

因此,据我所知,Java语言规范允许输出1 0。