2016-02-01 121 views
2

我在期待這段代碼是線程安全的。我跑了幾次,但得到不同的結果。但是,如果我取消註釋sleep(1000)部分,則每次打印10000次(至少從我的測試運行結果中)。這段代碼不是線程安全的嗎?

那麼,怎麼了?難道這與thread.join()有關嗎?

public class Test implements Runnable{ 

    private int x; 

    public synchronized void run(){ 
      x++; 
    } 

    public static void main(String args[]){ 
     Test test = new Test(); 
     Thread thread = null; 
     for (int i = 0; i < 10000; i++) { 
      thread = new Thread(test); 
      try { 
       thread.join(); 
      } catch (InterruptedException e) {} 
      thread.start(); 
     } 
//  try { 
//   Thread.sleep(1000); 
//  } catch (InterruptedException e) { 
//   e.printStackTrace(); 
//  } 
     System.out.println(test.x); 
    } 
} 

編輯:哎呀,我的壞。我誤解了Thread#加入函數的方式。與run()方法同步是一個壞主意。

+3

爲什麼'thread.join()'在'thread.start()'之前? 'join()'表示「阻塞直到線程結束」。這隻有在線程啓動後纔有意義。 – khelwood

+1

同步運行方法在這裏並不是真正的問題(除了主要方法直接訪問x的部分,正如我的答案指出的那樣);因爲所有線程共享同一個測試同步實例可以正常工作。 –

回答

2

有兩個問題在這裏:

  • 在主線程所有工作線程之前完成的競爭條件。

  • 內存可見性問題,其中主線程不能保證看到x的更新值。

線程#加入是使用Object#wait實現的。使用的條件變量是線程活着標誌:

groovy:000> new Thread().isAlive() 
===> false 

的Thread.join正在檢查活着的標誌線開始前,所以的IsAlive返回false和聯接返回前線程可以啓動。計數器最終還是會增加,但由於該線程沒有發生連接,因此主線程可能會在所有線程執行前打印出x的結果。

添加睡眠可以讓所有線程有足夠的時間完成這個x是主線程打印出來的時間。

除了競態條件之外,還有一個內存可見性問題,因爲主線程直接訪問x並且沒有使用與其他線程相同的鎖。你應該使用​​關鍵字添加一個訪問到您的Runnable:

public class Test implements Runnable{ 

    private int x; 

    public synchronized void run(){ 
     x++; 
    } 

    public synchronized int getX() { 
     return x; 
    } 

,改變的主要方法使用訪問:因爲它們取決於如何侵略性

System.out.println(test.getX()); 

內存知名度的問題可能不明顯JVM是關於緩存和優化。如果您的代碼在生產環境中針對不同的JVM實現運行,並且沒有充分防範這些問題,那麼您可能會發現無法在PC上本地複製的錯誤。

使用AtomicInteger將簡化此代碼,並允許解決內存可見性問題,同時刪除同步。

+0

感謝您的洞察力。這真的很有幫助。 – du369

1

您不會將同步添加到運行方法。每個線程都有自己的。

您必須同步可變的共享數據。在你的情況下,這是integer x。您可以同步獲取/設置或使用AtomicInteger

4

thread.join()應在thread.start()之後調用。

join()表示「阻塞直到線程結束」。這隻有在線程啓動後纔有意義。

推測你的Thread.sleep()調用實際上等待所有線程(你實際上沒有加入)完成足夠長的時間。沒有它,當您打印出x的值時,線程可能不會全部完成。