2014-02-05 39 views
11

這個問題是來自什麼可以強制刷新非易失性變量?

啓發藉此JVM

java version "1.7.0_45" 
Java(TM) SE Runtime Environment (build 1.7.0_45-b18) 
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode) 

和下面的代碼

public class Driver implements Runnable { 
    public static void main(String args[]) throws Exception { 
     Driver driver = new Driver(); 
     Thread thread = new Thread(driver); 
     thread.start(); 
     Thread.sleep(100); // make sure threads are unsynced 
     driver.stop = true; 
    } 

    boolean stop = false; // not volatile 

    @Override 
    public void run() { 
     // Loop until stop is set to true. 
     while (!stop) { 
      doSomething(stop); 
      // System.out.println("random string"); 
     } 
     System.out.println("done"); 
    } 

    public static void doSomething(boolean value) { 
     /*ByteArrayOutputStream out = new ByteArrayOutputStream(); 
     try { 
      out.write(new byte[] {1}); 
     } catch (IOException e) { 
      e.printStackTrace(); 
     }*/ 
    } 
} 

當我運行的代碼中,thread線程永遠不會看到stop的值爲true,因此永遠不會退出while循環(最終它可能會,但在短期內它不會)。

注意在這個呼叫

doSomething(stop); 

在字節代碼中使用的stop,這相當於其在while循環使用,即。有一個字節碼指令

11: getfield  #14     // Field stop:Z 

所以我不明白爲什麼這應該有任何效果。

現在,如果你取消第

// System.out.println("random string"); 

一個或內doSomething(boolean)的代碼,然後將代碼執行約100毫秒(該Thread.sleep()的時間),然後自然結束,即得。 stop的值同步

我不知道解釋這種行爲。似乎使用OutputStream會影響線程刷新其字段值的視圖的方式和時間。這是可能的還是我錯過了什麼?

什麼可以強制刷新非易失性變量值的線程視圖?

+0

這個問題的一個單詞的答案是「什麼」。您從未保證不會看到更新。所以*任何*都可以讓你看到一個。 –

回答

12

沒什麼,我知道解釋這種行爲。看起來像使用OutputStream會影響線程刷新字段值的視圖的方式和時間。

System.outPrintStream不是OutputStream。在PrintStream所有方法在流同步:

public void println(String x) { 
    synchronized (this) { 
     print(x); 
     newLine(); 
    } 
} 

每當你進入一個​​塊,有一個線程這是造成布爾字段要更新的讀取內存同步事件。

什麼可以強制刷新非易失性變量值的線程視圖?

因此輸入​​塊會導致這種情況。也從任何volatile讀取讀取同步。一個線程也可以從它的CPU中換出,迫使它在下一個時間片時重新加載它的緩存。


從與@Voo討論增加更多的細節,這是在JLS不解決。然而,在詳細介紹JMM需求的JSR-133文件中提到了這一點。引用來自JSR-133 FAQ

但是,有更多的同步比互斥。同步確保線程在同步塊之前或期間寫入的內存以可預測的方式顯示給在同一監視器上同步的其他線程。在我們退出一個同步塊之後,我們釋放監視器,將緩存刷新到主內存,以便該線程所做的寫入對其他線程可見。在我們輸入一個同步塊之前,我們需要獲取監視器,這會導致本地處理器緩存失效,從而使變量從主內存中重新加載。然後,我們將能夠看到之前版本中可見的所有寫入。

因此,即使是非同步的,非易失性領域打交道時,如果一個線程進行更改,然後寫入到一個volatile場或離開​​塊,即場將予以公佈。如果此後,另一個線程從任何volatile字段讀取或輸入任何​​塊,則會看到該字段已更新。

這樣做不是表示有任何訂單保證。與內存同步相比,在同步訂單周圍存在很多混淆。如果兩個線程在兩個不同的對象上進行同步,則由於數據競爭而無法保證操作順序。但是,如果字段已由線程-A在寫入內存同步事件之前更新,那麼它將被髮布。而如果該字段已被另一個線程發佈之前,線程-B有一個讀取內存同步事件,那麼它將被更新。

+1

你在哪裏閱讀JMM中的保證,即進入同步塊將更新內存的*整個*視圖,而不僅僅是與進入該監視器的最後一個線程建立一個前後關係。 (即只保證您看到一個線程在離開監視器時看到的所有更新)實際上,在常見的CPU上發生這種情況是正確的,但我沒有看到JLS的任何保證。誠實的問題,而不是攻擊 - 我可能真的忽略了一些細節,這很複雜! – Voo

+0

是的,它很複雜@Voo。 JLS沒有具體提及它,但關於JMM的JSR-133文檔更具體。以下是關於它的常見問題解答:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#synchronization – Gray

+0

大的困惑在於同步順序。關係之前沒有_guarantee_發生任何事情。然而,如果一個線程進入一個'synchronized'塊(或從volatile域中讀取),則其所有本地緩存​​內存都將失效,因此它將看到所有發佈到中央內存的值。 – Gray

6

有在這裏玩了兩兩件事:

第一:System.out.println是一個同步的方法,這意味着每次執行它時,你必須獲取和釋放監視器。獲取顯示器會與上一次發佈顯示器時建立一個發生之前的關係。

純粹來自JLS(更準確地說JMM),實際上它不足以確保您看到stop的更新,因爲其他線程從未獲取相同的顯示器。但從純粹的實際角度來看,事實證明許多CPU(當然還有x86)不會做出如此細緻的區分,所以即使規範不能保證,您仍會看到更新的完整內存。爲了通過規格保證你會看到更新,設置stop=true的線程會自己打印一些東西,從而獲得相同的鎖定。然後保證下次打印時您必須看到更新的停止值。

什麼可能是更可能是這裏的原因是,println是一個本地方法和可能不是由JVM內聯,因此它不能做所有相關變量的逃逸分析,因此有假設println更改stop的值。因此它不能改變

while (!stop) { 
    doSomething(stop); // doSomething does not change value of stop variable 
} 

if (stop) return; 
while (true) 
     doSomething(stop); // doSomething does not change value of stop 
+1

這聽起來似乎合情合理。我把它稱爲「意外同步」,讓你的代碼依賴於它對我來說似乎是一個壞主意。在處理多線程應用程序中的共享可變狀態時,最好的方法是不要與內存模型作鬥爭。添加必要的同步並完成它。 –

+0

@Bob哦,這是一個非常糟糕的主意,我同意。在多線程代碼中引入錯誤很容易,如果你不想特別聰明 - 只要使其變得不穩定或使用鎖定即可!如果我不得不猜測,我會說這實際上在Alpha上被破壞了,因爲嘿,ISA很瘋狂;)(我必須認真思考itanium和aarch64,但我認爲*可能會成爲一個問題這兩個也是)。 – Voo