2016-05-15 89 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。