问题描述
我正在阅读 Chapter 17. Threads and Locks of JLS 并且以下关于 Java 中顺序一致性的陈述对我来说似乎不正确:
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。
他们将数据竞争定义为:
当一个程序包含两个冲突的访问(第 17.4.1 节)并且不是按发生之前的关系排序时,它被称为包含数据竞争。
他们将冲突访问定义为:
如果至少有一次访问是写操作,则称对同一变量的两次访问(读取或写入)发生冲突。
最后,他们对发生在之前的关系进行了跟踪:
写入易失性字段(第 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;
在上面的代码中,我还用缩进展示了线程的代码在运行时是如何交错的。
所以,据我所知,这个程序:
- 没有数据竞争:线程 2 中 v1 和 v2 的读取与线程 1 中的写入同步
- 可以输出
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;
解决方法
您的错误在第 1 点:v1
和 v2
的读取不是同步。
happens-before 关系仅通过与 vv
的交互创建,因此例如在这种情况下,如果您添加了 vv
到打印语句的开头,您肯定不会看到 vv=20,v2=4
。由于您忙于等待 vv
变为非零但随后不再与它交互,唯一的保证是您将看到在它变为非零之前发生的所有效果( 1 和 2 的赋值)。您也可能会看到未来的影响,因为您没有任何进一步的发生之前。
即使你把所有的变量都声明为 volatile,你仍然有可能输出 v1=1,v2=4
因为变量的多线程访问没有定义的顺序,全局序列可以是这样的:
- T1:写
v1=1
- T1:写
v2=2
- T1:写
vv=10
(线程 2 不能在此之前退出 while 循环,并且保证看到所有这些效果。) - T2:读取
vv=10
- T2:读取
v1=1
- T1:写
v1=3
- T1:写
v2=4
- T2:读取
v2=4
在每一步之后,内存模型保证所有线程都会看到相同的 volatile 变量值,但是你有一个数据竞争,那就是因为访问不是原子的(分组) em>。为了确保您在一个组中看到它们,您需要使用一些其他方式,例如在 synchronized
块中执行或将所有值放入一个记录类中并使用 volatile
或 { {1}} 以换出整个记录。
正式地,JLS 定义的数据竞争包括操作 T1(写入 v1=3)和 T2(读取 v1)(以及 v2 上的第二个数据竞争)。这些是冲突访问(因为 T1 访问是写入),但是虽然这两个事件都发生在 T2(读取 vv)之后,但它们之间没有相关的顺序.
,证明自己错了实际上比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下是“同步的”,所有这些都在正确的 chapter in the JSL 中定义。接受的答案说 synchronizes-with
不是一个实际的术语,但这是错误的。 (除非我没有理解意图或其中有错误)。
由于你没有这样的特殊动作来建立同步顺序(简称SW
),在Thread1
和Thread2
之间,接下来的一切都像纸牌城堡一样落下,没有意义了。
您提到了 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 块中可以看到什么。你从简单开始,from here:
如果 x 和 y 是同一个线程的动作,并且 x 在程序顺序中排在 y 之前,则 hb(x,y)。
好的,所以 v1 = 1
happens-before
v2 = 2
和 happens-before
vv = 10
。通过这种方式,我们可以在同一线程中的操作之间建立hb
。
我们可以通过 synchronizes-with
顺序、the proper chapter 和适当的规则“同步”不同的线程:
写入易失性变量 v 同步所有线程对 v 的后续读取
这样我们就在两个独立线程之间建立了一个 SW
顺序。这反过来又允许我们现在构建一个 HB
(之前发生过),因为 proper chapter 和另一个适当的规则:
如果一个动作 x 与后续动作 y 同步,那么我们也有 hb(x,y)。
所以现在你有了一个链:
(HB) (HB) (HB) (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....
所以只有现在,您才证明 if 块将读取 r1 = 1
和 r2 = 2
。并且因为 volatile
提供了顺序一致性(无数据竞争),每个将读取 vv
为 10
的线程肯定也会将 v1
读取为 {{1 }} 和 1
为 v2
。