2016-09-08 59 views
2

之前我有一個關於如何使對象保證爲Java存儲模型爲線程安全的問題。在構造函數中進行同步以使其發生 - 在

我讀過很多說,寫一個同步範圍在構造函數中沒有意義,但爲什麼不呢?是的,只要構造中的對象不在線程之間共享(不應該是),除了構造線程之外的任何線程都可以達到任何同步(this){...},所以在那裏不需要在構造函數中創建該範圍以排除它們。但同步的範圍不僅僅是排除在外;他們也被用來創造發生之前的關係。 JLS.17.4

這裏是一個示例代碼,使我的觀點清晰。

public class Counter{ 

    private int count; 

    public Counter(int init_value){ 
     //synchronized(this){ 
      this.count = init_value; 
     //} 
    } 

    public synchronized int getValue(){ 
     return count; 
    } 

    public synchronized void addValue(){ 
     count++; 
    } 
} 

想想線程t0創建一個Counter對象,另一個線程t1使用它的情況。如果在構造函數中有同步語句,它顯然會保證是線程安全的。 (因爲同步作用域中的所有操作之間都有一個「先發生之前」關係)。但是,如果不是,即沒有同步化語句,那麼Java存儲器模型仍然保證可以通過t1看到count的t0初始化寫操作?我想不是。這就像f.y在JLS.17.5的示例代碼17.5-1中可以看到0一樣。與JSL.17.5-1的情況不同,現在第二個線程僅從同步方法訪問字段,但我認爲同步語句在這種情況下無法保證有效。 (他們不會在t0之前創建任何發生在任何動作之前的關係)。有人說,關於一個發生的規則 - 在構造函數結束之前發生的邊緣保證了它,但規則似乎只是說構造函數發生 - finalize()之前。

然後,我應該在構造函數中寫入synchronized語句以使對象線程安全嗎?或者是否存在關於我錯過的Java內存模型的一些規則或邏輯,實際上並不需要這些?如果我是真的,甚至openjdk的Hashtable(雖然我知道它已經過時)似乎不是線程安全的。

還是我錯了線程安全的定義和關於併發策略?如果我通過線程安全的方式將Counter對象從t0傳送到t1,例如通過一個易變的變量,似乎沒有問題。 (在這種情況下,t0的構造發生在易失性寫入之前,發生在t1發生易失性讀取之前,在t1發生之前發生)。我應該始終傳輸線程安全對象(但不是不可變的)之間的線程通過一種方式,導致發生之前的關係?

+1

「考慮一個線程t0創建一個Counter對象,另一個線程t1使用它的情況。」在構造函數返回t0之前,t1如何引用'Counter'? – bradimus

+2

您的問題已通過安全發佈解決。 – assylias

回答

5

如果該對象已被安全地發佈(例如,通過將其實例化爲someVolatileField = new Foo()),那麼在構造函數中不需要同步。如果不是,那麼在構造函數中的同步是不夠的。

關於這個java關於java併發興趣列表a few years back的討論有些冗長;我會在這裏提供總結。 (完全披露:我開始討論了,並且涉及到整個它。)

請記住,發生之前的邊緣只適用於釋放鎖定的一個線程與獲取它的後續線程之間。所以,讓我們說你有:

someNonVolatileField = new Foo(); 

有有三種顯著成套動作的位置:

  1. 對象被分配,與所有的字段設置爲0 /空
  2. 構造運行,它包括對象監視器的獲取和釋放
  3. 對象的引用被分配給someNonVolatileField

假設另一個線程使用該引用,並調用synchronized doFoo()方法。現在,我們添加兩個動作:

  • 讀取someNonVolatileField參考
  • 調用doFoo(),其包括獲取與對象監視器
  • 的釋放由於出版物某些NonVolatileField並不安全,系統可以進行大量重新排序。特別是,讀線程可以看到事情發生的順序如下:

    1. 對象被分配,與所有的字段設置爲0 /空
    2. 對象的引用被分配到someNonVolatileField
    3. 閱讀所述someNonVolatileField參考
    4. 調用doFoo(),其包括獲取與對象監視器的釋放
    5. 構造的運行,其中包括對象監視器的獲取和釋放

    在這種情況下,仍然存在發生之前的邊緣,但是從相反的方向開始。具體來說,doFoo()的調用正式發生在構造函數之前。

    這不會給你買個位;這意味着任何同步的方法(或塊)都可以保證看到構造函數的全部效果,或者沒有看到這些效果。它不會只看到構造函數的一部分。但在實踐中,你可能想要保證你看到構造函數的效果;畢竟,這就是爲什麼你寫了構造函數。

    您可以通過doFoo()不同步來解決這個問題,而是設置一些旋轉循環等待一個表示構造函數已經運行的標誌,然後是一個手動synchronized(this)塊。但是,當你達到這種複雜程度時,最好只說「這個對象是線程安全的,假設它的初始發佈是安全的。」對於大多數可變類來說,這是事實上的假設,它們將自己記錄爲線程安全的;不可變的可以使用final字段,即使面對不安全的發佈,它也是線程安全的,但不需要顯式同步。

    +1

    @RealSkeptic假設JVM 1。5+,在第一個寫入引用的線程和從它讀取的任何後續線程之間存在完整的發生。 – yshavit

    +0

    拋開一個問題。從理論上講,'同步(this)'可以消除,因爲'這個'似乎沒有逃脫嗎? –

    +0

    @RealSkeptic如果A發生在B之前,那麼看到B的線程必須看到A之前的所有動作(現在看不到確切的jls ref,對不起......在第17章的某處)。 )。因此,在這種情況下,讀取操作會看到對引用的寫入以及寫入線程之前的所有操作 - 特別是'new'調用和構造函數。 – yshavit

    相關問題