11

我在一些OSS單元測試中很頻繁地看到這段代碼,但它是否安全? while循環保證能夠看到invoc的正確值嗎?是java中整數線程安全讀取的不同步嗎?

如果否;書呆子指向誰也知道哪個CPU架構可能會失敗。

private int invoc = 0; 

    private synchronized void increment() { 
    invoc++; 
    } 

    public void isItThreadSafe() throws InterruptedException { 
     for (int i = 0; i < TOTAL_THREADS; i++) { 
     new Thread(new Runnable() { 
      public void run() { 
      // do some stuff 
      increment(); 
      } 
     }).start(); 
     } 
     while (invoc != TOTAL_THREADS) { 
     Thread.sleep(250); 
     } 
    } 
+0

不能說100%,但它看起來像非易失性字段的非同步讀取不保證讀取'invoc'的最近值(即使* *刷新*由於'increment()'方法中的''synchronized''關鍵字。)儘管如此,不太瞭解Java內存模型足以說明問題。 – dlev

+0

@dlev就是這樣的問題;) – krosenvold

+0

我正要回答,「它一直對我有效!」。但也發現http://stackoverflow.com/questions/1006655/are-java-primitive-ints-atomic-by-design-or-by-accident其中說「是」的整數,而「也許」多頭和雙打。 –

回答

17

不,它不是線程安全的。 invoc需要聲明爲volatile,或者在同一個鎖上同步時訪問,或者更改爲使用AtomicInteger。只是使用synchronized方法來增加invoc,但不能同步讀取,這不夠好。

JVM進行了很多優化,包括特定於CPU的緩存和指令重新排序。它使用volatile關鍵字和鎖定來決定何時可以自由優化以及何時必須具有供其他線程讀取的最新值。所以當讀者不使用鎖時,JVM不會知道不會給它一個陳舊的值。

Java併發實踐(3.1.3節)這句話討論這兩種寫入和讀取需要如何進行同步:

內在鎖可以用來保證一個線程看到的效果另一個以可預測的方式進行,如圖3.1所示。當線程A執行一個同步塊,並且隨後線程B進入一個由同一個鎖保護的同步塊時,在釋放鎖之前A對A可見的變量的值保證在獲得鎖時對B可見。換句話說,當B執行一個由同一個鎖保護的同步塊時,在一個同步塊中或之前執行的所有操作都對B可見。沒有同步,就沒有這種保證。

下一節(3.1.4)覆蓋用揮發性:

Java語言還提供了一種替代方案中,同步,易失性變量,較弱形式以確保更新給一個變量是可預見的傳播到其他線程。當一個字段被聲明爲volatile時,編譯器和運行時會注意到這個變量是共享的,並且它的操作不應該與其他內存操作重新排序。易失性變量不會緩存在寄存器或高速緩存中,因爲它們對其他處理器是隱藏的,因此讀取volatile變量總是會返回任何線程的最新寫入。

當我們的桌面上都有單CPU機器時,我們會編寫代碼,直到在多處理器機箱上運行(通常是在生產環境中)纔會出現問題。引起可見性問題的一些因素,如CPU本地緩存和指令重新排序等,都是您所期望的任何多處理器機器的事情。但是,消除任何機器上明顯不需要的指令都可能發生。沒有任何東西強迫JVM永遠讓讀者看到變量的最新值,而是受到JVM實現者的擺佈。所以在我看來,這個代碼對於任何CPU架構都不是一個好的選擇。

2

好吧!

private volatile int invoc = 0; 

會做的伎倆。

並參見Are java primitive ints atomic by design or by accident?哪些網站的一些相關的java定義。顯然int是好的,但雙重&可能不是。


編輯,附加。問題問:「看到invoc的正確值?」。什麼是「正確的價值」?與時空連續體一樣,線程之間並不存在同時性。上述其中一個帖子指出,該值最終會被刷新,而另一個線程將會得到它。代碼是否「線程安全」?我會說「是的」,因爲在這種情況下,基於不確定性的排序,它不會「行爲不端」。

+1

我不確定上述問題中的任何引用實際上是否覆蓋了對不同線程的可見性 – krosenvold

+1

沒有形而上學,我認爲有理論上的機會外部循環將永遠不會看到任何東西,但0。 – krosenvold

0

據我瞭解代碼應該是安全的。字節碼可以重新排序,是的。但最終invoc應該再次與主線程同步。 Synchronize保證invoc正確遞增,因此在某些寄存器中有一致的invoc表示。在某個時候,這個值將被刷新,小測試成功。

這當然不是很好,我會用我投,因爲它的氣味會解決這樣的代碼的答案去。但考慮一下,我會認爲它是安全的。

+0

它更多的是關於可見性,更少的重新排序。 VM **可以**優化內存讀取完全並將_invoc_視爲訪問不同步/不穩定的線程中的常量。所以這個問題必須得到解決,儘管它有時甚至經常有效。 – Boris

+0

我知道我應該相信我的直覺:-) –

1

理論上講,讀取可能會被緩存。 Java內存模型中沒有任何內容可以阻止這一點

實際上,這是不太可能發生的(在您的特定示例中)。問題是,JVM是否可以通過方法調用進行優化。

read #1 
method(); 
read #2 

對於JVM到原因,讀#2可以重用的讀#1(其可以存儲在一個CPU寄存器)的結果,它必須知道肯定method()不包含同步操作。這通常是不可能的 - 除非內聯method(),並且JVM可以從分層代碼中看到在讀#1和讀#2之間沒有同步/易失性或其他同步操作;那麼它可以安全地消除讀取#2。

現在在你的例子中,方法是Thread.sleep()。實現它的一種方式是在特定時間內忙於循環,具體取決於CPU頻率。然後JVM可以內聯它,然後消除讀取#2。

但是當然這種sleep()的實現是不現實的。它通常作爲調用OS內核的本地方法來實現。問題是,JVM可以通過這種本地方法進行優化。

即使JVM具有某些本地方法的內部工作知識,因此可以在它們之間進行優化,但不可能這樣處理sleep()sleep(1ms)需要數百萬個CPU週期才能返回,但實際上沒有任何優化措施可以節省幾次讀取。

-

這種討論揭示了數據競爭的最大的問題 - 它需要太多的精力來思考它。一個程序不一定是錯誤的,如果它沒有「正確同步」,但是證明它沒有錯是不是一件容易的事。如果程序正確同步並且不包含數據競爭,生活就簡單多了。

+0

奇怪的是,我幾乎可以肯定我看到了這個確切的案例發生,理論與否。 – krosenvold

0

如果你不需要使用「int」,我會建議AtomicInteger作爲一個線程安全的選擇。

相關問題