假设有两个没有同步的线程,一个设置n = 1
另一个执行method()
。
在下文中,“read”始终指的是字段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之前,仍然可以在读取1之前重新排序读取2,因为它不会改变线程内执行的语义。
这种推理是否正确?
答案 0 :(得分:28)
发生之前并不意味着两个任意操作的顺序。更确切地说,之前发生的最重要的事情是在发生 - 在一致性之前绑定写入和读取。值得注意的是,它告诉读取观察的写入内容:最后一次写入发生 - 在顺序之前,或者在发生之前(竞赛)中没有排序的任何其他写入。请注意,两次连续读取可能会看到从不同(racy)写入获得的不同值,而不会违反该要求。
E.g。 JLS 17.4.5说:
应该注意到之前发生的关系 两次行动之间并不一定意味着必须采取行动 在实现中按顺序放置。如果重新排序产生 结果与法律执行一致,这不违法。
数据竞争令人毛骨悚然:racy读取可以在每次读取时返回令人惊讶的数据,而Java内存模型会捕获这些数据。因此,更精确的答案是产生(1,0)的执行不违反Java内存模型约束(同步顺序一致性,同步顺序 - 程序顺序一致性,发生在一致性,因果关系要求之外),因此允许。
实施方式:在硬件上,两个负载都可以在不同的时间启动和/或到达内存子系统,无论它们的程序顺序如何,因为它们是独立的;在编译器中,指令调度也可以忽略独立读取的程序顺序,将负载暴露给硬件,反直觉"顺序。
如果您希望在程序顺序中观察到读取,则需要更强大的属性。 JMM将该属性赋予同步操作(在您的示例中,使变量volatile
成为可能),这将总计同步顺序中的操作与一致与程序顺序。在这种情况下,(1,0)将被禁止。
very special jcstress testcase上的插图(请参阅警告的完整来源):
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上,yield(1,0),oops:
[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...
使Holder.a
volatile会使(1,0)消失。
答案 1 :(得分:4)
我们有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没有顺序一致的执行。
但是,Java语言规范只保证所有执行都是顺序一致的,如果它们没有数据竞争,我们的程序不是。具体来说,规范写道:
更具体地说,如果两个动作共享一个发生在之前的关系,那么它们不一定必须按照那个顺序发生在它们不与之共享的任何代码中。例如,在具有另一个线程中的读取的数据争用中的一个线程中的写入可能看起来不按顺序发生到那些读取。
并且
我们说如果在执行跟踪的发生之前的部分顺序中,允许变量v的读取r观察到写入w:
r未在w之前订购(即,不是hb(r,w)),
没有干预写v'(即没有写w'到v使得hb(w,w')和hb(w',r))。
在我们的例子中,允许两个读取同时观察0和1,因为没有插入写入。
因此,据我所知,Java语言规范允许输出1 0。