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;
在上面的代码中,我还用缩进展示了线程代码在运行时是如何交错的。
所以,据我了解,这个程序:
- 没有数据竞争:线程 2 中 v1 和 v2 的读取与线程 1 中的写入同步
- 可以输出
v1=1 v2=4
(违反顺序一致性)
因此,来自 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 中:v1
和 v2
的读取未 与 [= 同步60=].
通过与 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 变量值,但是你有一个数据竞争,那就是 因为访问不是原子的(分组)。为了确保您在一组中看到它们,您需要使用一些其他方法,例如在 synchronized
块中执行或将所有值放入记录 class 并使用 volatile
或 AtomicReference
换出整个记录。
形式上,JLS 定义的数据竞争包括操作 T1(写入 v1=3)和 T2(读取 v1)(以及 v2 上的第二个数据竞争)。这些是冲突访问(因为 T1 访问是写入),但是虽然这两个事件都发生在 T2(读 vv)之后,但它们的顺序并不 彼此.
证明你错了实际上比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下“同步”,所有这些规则都在适当的 chapter in the JSL 中定义。接受的答案说 synchronizes-with
不是实际术语,但这是错误的。 (除非我误解了意图或其中有错误)。
由于您没有这种特殊的操作来建立同步顺序(简称SW
),所以在Thread1
和Thread2
之间,接下来的一切就像一座城堡卡片,不再有意义。
你提到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 = 2
和 happens-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 = 1
和 r2 = 2
。并且因为 volatile
提供顺序一致性(没有数据竞争),每个将 vv
读取为 10
的线程肯定也会将 v1
读取为 1
和 v2
为 2
.
我正在阅读 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;
在上面的代码中,我还用缩进展示了线程代码在运行时是如何交错的。
所以,据我了解,这个程序:
- 没有数据竞争:线程 2 中 v1 和 v2 的读取与线程 1 中的写入同步
- 可以输出
v1=1 v2=4
(违反顺序一致性)
因此,来自 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 中:v1
和 v2
的读取未 与 [= 同步60=].
通过与 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 变量值,但是你有一个数据竞争,那就是 因为访问不是原子的(分组)。为了确保您在一组中看到它们,您需要使用一些其他方法,例如在 synchronized
块中执行或将所有值放入记录 class 并使用 volatile
或 AtomicReference
换出整个记录。
形式上,JLS 定义的数据竞争包括操作 T1(写入 v1=3)和 T2(读取 v1)(以及 v2 上的第二个数据竞争)。这些是冲突访问(因为 T1 访问是写入),但是虽然这两个事件都发生在 T2(读 vv)之后,但它们的顺序并不 彼此.
证明你错了实际上比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下“同步”,所有这些规则都在适当的 chapter in the JSL 中定义。接受的答案说 synchronizes-with
不是实际术语,但这是错误的。 (除非我误解了意图或其中有错误)。
由于您没有这种特殊的操作来建立同步顺序(简称SW
),所以在Thread1
和Thread2
之间,接下来的一切就像一座城堡卡片,不再有意义。
你提到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 = 2
和 happens-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 = 1
和 r2 = 2
。并且因为 volatile
提供顺序一致性(没有数据竞争),每个将 vv
读取为 10
的线程肯定也会将 v1
读取为 1
和 v2
为 2
.