2016-05-15 91 views
16

假设有两个线程没有同步,一个集合n = 1另一个执行method()重新排序的读取

在下面的“读”总是指读取的字段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,但它仍然是可能的阅读2被重新排序之前阅读1,因为它不会改变线程内执行的语义。

这个推理是否正确?

+2

是的,这是正确的。但是,编译器不是“重新排序”线程。编译器只是编译。 – Elyasin

+0

我怀疑'1,0'是可能的。这看起来非常非常错误。 – luk2302

+0

我会很惊讶地看到1,0。你真的看过这个输出吗? – bhspencer

回答

28

发生以前并不意味着两个任意操作的顺序。更确切地说,发生的最重要的事情 - 之前所做的是绑定写入读取发生 - 在一致性之前发生。值得注意的是,它告诉读者可以观察哪些写操作:最后一次写操作发生在订单之前,或者其他任何未订购的写操作发生之前(竞争)。请注意,连续两次读取可能会看到从不同(写入)写入获得的不同值,而不会违反该要求。

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不重新排序负荷,产率(1,0)时,糟糕:

 [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挥发性会使(1,0)消失。

+0

只有当变量被多个线程访问时,您才需要volatile。在这种情况下,调用方法()在一个线程中执行。所以他们共享相同的线程本地缓存。两个println执行都可以看到n的每次更改。 –

+2

OP的问题具体说明了一个线程正在设置'n = 1',另一个线程正在读'n'。在单线程的情况下,没有反复写入,并且线程被迫观察到最近写入'n'(这可能被认为是“在两次读取之前发生”)。 –

4

我们有4个动作,形成以下的之前发生图:

+-------+  ? +-------+ 
| n = 0 | ----> | n = 1 | 
+-------+   +-------+ 
    | 
    |? 
    v 
    +---+    +---+ 
    | n |  ----> | n | 
    +---+    +---+ 

既然你不给那将n初始化代码,它不知道是否N = 0的之前发生N = 1 ,并且在n的第一次读取之前是否发生n = 0。如果这些边不存在,则(n = 1,n,n = 0,n)是顺序一致的执行顺序,并且输出10是平凡的可能的。

如果已知为n = 0之前发生n = 1时,不存在一致的顺序执行与输出1 0

然而,Java语言规范只保证所有执行是如果顺序一致他们没有数据竞赛,我们的计划不是。具体而言,该规范中写道:

更具体地说,如果两个动作共享的之前发生关系,他们并不一定要出现在以发生于它们不共享happens-任何代码关系之前。例如,在一个线程中写入数据竞争,而在另一个线程中读取数据时,可能会出现这些读取不按顺序发生的情况。

而且

我们说一个变量v的读R能够观看到的写◆如果到v,在之前发生的执行跟踪的偏序:

  • R不是之前W¯¯有序(即,它是不是这样认为HB(R,W)),以及

  • 没有干涉写W '为v(即没有写W' 到v的那hb(w,w')和hb(w',r))。

在我们的情况下,两个读数都是能够观看到0和1,因为没有干涉写。

因此,据我所知,Java语言规范允许输出1 0。