Java 内存模型:关于顺序一致性的 JLS 声明似乎不正确

Java Memory Model: a JLS statement about sequential consistency seems incorrect

我正在阅读 Chapter 17. Threads and Locks of JLS,以下关于 Java 中顺序一致性的陈述对我来说似乎不正确:

If a program has no data races, then all executions of the program will appear to be sequentially consistent.

他们将数据竞争定义为:

When a program contains two conflicting accesses (§17.4.1) that are not ordered by a happens-before relationship, it is said to contain a data race.

他们将冲突访问定义为:

Two accesses to (reads of or writes to) the same variable are said to be conflicting if at least one of the accesses is a write.

最后他们有以下关于 happens-before 关系:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

我对第一个陈述的问题是我想我可以想出一个 Java 没有数据竞争并允许顺序不一致执行的程序:

// Shared code
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Thread1       Thread2
   v1 = 1;
   v2 = 2;
   vv = 10;      while(vv == 0) {;}
                 int r1 = v1;
                 int r2 = v2;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

在上面的代码中,我还用缩进展示了线程代码在运行时是如何交错的。

所以,据我了解,这个程序:

因此,来自 JLS 的初始语句

If a program has no data races, then all executions of the program will appear to be sequentially consistent.

我觉得不对。

我是不是遗漏了什么或者哪里弄错了?

编辑: 用户 chrylis-cautiouslyoptimistic 正确指出我给出的代码可以输出 v1=1 v2=4 具有顺序一致性——线程代码中的行应该交织有点不同。

所以这里是稍微修改的代码(我改变了读取的顺序),顺序一致性无法输出v1=1 v2=4,但一切仍然适用。

// Shared code
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Thread1       Thread2
   v1 = 1;
   v2 = 2;
   vv = 10;      while(vv == 0) {;}
                 int r2 = v2;
                 int r1 = v1;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

您的错误在要点 #1 中:v1v2 的读取未 与 [= 同步60=].

通过与 vv 的交互, 创造了先于 关系,因此例如在这种情况下,如果您将 vv 添加到打印语句的开头,您肯定不会看到 vv=20,v2=4。由于您忙于等待 vv 变为非零值 但随后不再与它交互 ,唯一的保证是您将看到它变为之前发生的所有效果非零(1 和 2 的赋值)。你也可能看到未来的影响,因为你没有任何进一步的先发

即使你把所有的变量声明为volatile,你仍然可以输出v1=1,v2=4 因为多线程访问变量没有定义的顺序,全局顺序可以这样:

  1. T1:写入v1=1
  2. T1:写入v2=2
  3. T1:写入vv=10(线程2不能退出while循环,保证看到所有这些效果。)
  4. T2:阅读vv=10
  5. T2:阅读v1=1
  6. T1:写入v1=3
  7. T1:写入v2=4
  8. T2:阅读v2=4

在每个步骤之后,内存模型保证所有线程将看到相同的 volatile 变量值,但是你有一个数据竞争,那就是 因为访问不是原子的(分组)。为了确保您在一组中看到它们,您需要使用一些其他方法,例如在 synchronized 块中执行或将所有值放入记录 class 并使用 volatileAtomicReference 换出整个记录。

形式上,JLS 定义的数据竞争包括操作 T1(写入 v1=3)和 T2(读取 v1)(以及 v2 上的第二个数据竞争)。这些是冲突访问(因为 T1 访问是写入),但是虽然这两个事件都发生在 T2(读 vv)之后,但它们的顺序并不 彼此.

证明你错了实际上比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下“同步”,所有这些规则都在适当的 chapter in the JSL 中定义。接受的答案说 synchronizes-with 不是实际术语,但这是错误的。 (除非我误解了意图或其中有错误)。

由于您没有这种特殊的操作来建立同步顺序(简称SW),所以在Thread1Thread2之间,接下来的一切就像一座城堡卡片,不再有意义。

你提到volatile,但同时要注意subsequent的意思:

A write to a volatile field happens-before every subsequent read of that field.

表示读会观察


如果您更改代码并建立 synchronizes-with 关系并因此隐式建立 happens-before,如下所示:

  v1 = 1;
  v2 = 2;
  vv = 10; 

             if(vv == 10) {
                int r1 = v1;
                int r2 = v2;
                // What are you allowed to see here?
             }

您可以开始推理在 if 块中可能看到的内容。你从简单开始,from here:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

好的,所以 v1 = 1 happens-before v2 = 2happens-before vv = 10。这样我们在同一个线程.

中的动作之间建立了hb

我们可以通过 synchronizes-with 命令、the proper chapter 和正确的规则“同步”不同的线程:

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread

这样我们就在两个独立的线程之间建立了一个SW顺序。这反过来又允许我们现在构建一个 HB(之前发生过),因为 proper chapter 和另一个正确的规则:

If an action x synchronizes-with a following action y, then we also have hb(x, y).

现在你有了一条链:

        (HB)          (HB)            (HB)                (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....

所以直到现在,您才有证据表明 if 块将读取 r1 = 1r2 = 2。并且因为 volatile 提供顺序一致性(没有数据竞争),每个将 vv 读取为 10 的线程肯定也会将 v1 读取为 1v22.