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

8
我正在阅读JLS的第17章。线程和锁定,其中关于Java中顺序一致性的以下说法似乎是不正确的:
如果程序没有数据竞争,则程序的所有执行将呈现出顺序一致性。
他们将数据竞争定义为:
当一个程序包含两个冲突访问(§17.4.1),这些访问未按happens-before关系排序时,就会发生数据竞争。
他们将冲突访问定义为:
对同一变量的两个访问(读取或写入)被称为冲突访问,如果其中至少一个访问是写入。
最后,他们关于happens-before关系有以下内容:
对易失字段(§8.3.1.4)的写入在随后对该字段的每次读取之前发生。
我对第一条语句的问题在于,我认为我可以想出一个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;

在上面的代码中,我还展示了线程代码如何在运行时交错。因此,我理解,这个程序:
- 没有数据竞争:Thread2中对v1和v2的读取与Thread1中的写入同步。 - 可以输出“v1=1 v2=4”(违反了顺序一致性)。
因此,JLS的初始陈述
如果一个程序没有数据竞争,那么程序的所有执行都将呈现为顺序一致的。
对我来说似乎是不正确的。我是否遗漏了什么或者我犯了错误?
编辑:用户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;

你如何知道线程的执行在运行时是如何交织的? - user207421
你是指上面的代码示例吗?我只是举了一个可能的执行示例作为例子。 代码示例的重点在于它与Java语言规范相矛盾:它符合其“顺序一致”的标准,但允许违反该标准的执行。 - JavaCur
2个回答

6
您的错误在于第一条要点:读取v1v2的操作没有同步
只有与vv的交互才会创建先于关系,因此例如在此情况下,如果您将vv添加到您的打印语句开头,您将保证不会看到vv=20,v2=4。由于您忙等待vv变为非零值,但随后不再与其交互,所以唯一的保证是您将看到它变为非零值之前发生的所有影响(1和2的赋值)。您还也可能看到未来的影响,因为您没有任何进一步的先于关系即使您将所有变量声明为易失性,仍然有可能输出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
在每个步骤之后,内存模型都保证所有线程将看到易失性变量的相同值,但您存在数据竞争,因为访问不是原子(分组)的。为了确保以组的方式查看它们,您需要使用其他手段,例如在synchronized块中执行或将所有值放入记录类并使用volatileAtomicReference来交换整个记录。
正式地,根据JLS定义,数据竞争由操作T1(write v1=3)和T2(read v1)(以及v2的第二个数据竞争)组成。这些是冲突访问(因为T1访问是写入),但是虽然这两个事件发生在T2(read vv)之后,但它们与彼此无序

我编辑了我的问题 - 我在结尾附加了一个稍微修改过的代码版本(我改变了Thread2中读取的顺序),现在即使我将所有变量声明为volatile,也不应该看到v1=1 v2=4 - JavaCur
我认为我理解了你的解释并且几乎同意所有内容。但是看起来你对数据竞争的定义与JLS中的定义不同。我想要理解JLS中的含义——因为这是所有Java实现必须遵守的规范。 - JavaCur
1
@JavaCur 已更新,明确标识构成数据竞争的访问操作。 - chrylis -cautiouslyoptimistic-
1
除了这里列出的术语,您指的是“未定义的术语”吗? - Eugene
@Eugene,我已经阅读了多个版本Java的整个JLS,但我不知道我怎么会忘记关系实际上是被定义的。(所涉及的行动仍然没有关联。) - chrylis -cautiouslyoptimistic-

2
实际上,证明你错误比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下进行“同步”,所有这些规则都在适当的JSL章节中定义。接受的答案说synchronizes-with不是一个实际的术语,但那是错误的(除非我误解了意图或其中有错误)。
由于您没有这样的特殊操作来建立Thread1Thread2之间的同步顺序(简称SW),随后的一切就像纸牌堆一样倒塌,不再有意义。
您提到了volatile,但同时要注意subsequent在其中的含义:

对volatile字段的写入发生在对该字段的每次后续读取之前。

它意味着将会观察到写入的读取。
如果您更改了代码并建立了一个“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块中可能看到的内容。从这里开始简单地理解

如果x和y是同一线程的动作,并且x在程序顺序上先于y,则hb(x,y)。

好的,所以v1 = 1 happens-before v2 = 2 happens-before vv = 10。这样我们就建立了在同一个线程中的动作之间的hb
我们可以通过synchronizes-with顺序来“同步”不同的线程,通过适当的章节和适当的规则:

对易失变量v的写入与任何线程后续读取v同步

这样,我们在两个独立的线程之间建立了一个 SW(Synchronized-with)顺序。反过来,由于适当章节和另一个适当的规则,现在可以建立一个 HB(happens before)关系:

如果动作 x 与后续动作 y 同步,则我们也有 hb(x, y)。

因此,现在你有一个链:
        (HB)          (HB)            (HB)                (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....

所以现在,你有证据表明如果块将读取r1 = 1r2 = 2。而且因为volatile提供了顺序一致性(没有数据竞争),每个读取vv10的线程也一定会读取v11v22

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