2012-04-20 141 views
10

我想確保根據Java內存模型正確理解「有效不可變對象」行爲。有效不可變對象

比方說,我們有我們要發佈爲有效不可改變一個可變類:

class Outworld { 
    // This MAY be accessed by multiple threads 
    public static volatile MutableLong published; 
} 

// This class is mutable 
class MutableLong { 
    private long value; 

    public MutableLong(long value) { 
    this.value = value; 
    } 

    public void increment() { 
    value++; 
    } 

    public long get() { 
    return value; 
    } 
} 

我們做到以下幾點:

// Create a mutable object and modify it 
MutableLong val = new MutableLong(1); 
val.increment(); 
val.increment(); 
// No more modifications 
// UPDATED: Let's say for this example we are completely sure 
//   that no one will ever call increment() since now 

// Publish it safely and consider Effectively Immutable 
Outworld.published = val; 

的問題是: Java內存模型是否保證所有線程都必須有Outworld.published.get() == 3

根據Java Concurrency In Practice這應該是真的,但請糾正我,如果我錯了。

3.5.3。安全出版習慣

要安全地發佈對象,對象的引用和對象的狀態必須同時對其他線程可見。 正確構造的對象可以通過以下方式安全地發佈:
- 從靜態初始化程序初始化對象引用;
- 將引用存儲到易失性字段或AtomicReference中;
- 將引用存儲到正確構造的對象的最終字段中;或
- 將對其的引用存儲到由鎖正確保護的字段中。

3.5.4。有效不可變的對象

安全地發佈有效的不可變對象可以安全地使用任何線程,而無需額外的同步。

+0

請顯示建立對象狀態的[* static initializer *](http://docs.oracle.com/javase/tutorial/java/javaOO/initial.html),該對象的狀態是可見的。 – trashgod 2012-04-20 23:07:09

回答

5

是的。在讀取之前,MutableLong上的寫入操作之後是happens-before關係(在易失性部分)。

(這可能是一個線程讀取Outworld.published並將其傳遞到另一個線程不安全的。從理論上說,可以看到以前的狀態。在實踐中,我沒有看到它的發生。)

+1

您的評論(第2段)似乎與您的答案(第1段)不符,而且與易失性閱讀鏈接之前發生的情況不符。你能詳細說明嗎? – assylias 2012-04-21 09:07:26

+2

假設線程T1安全地發佈到T2。從T1到T2有*發生之前*。但是,如果相同的對象從T2到T3不安全地發佈,那麼從T2到T3沒有* happen-before *,因此也沒有*在從T1到T3之前發生*之前發生。 – 2012-04-21 12:32:44

2

的問題是:Java內存模型是否保證所有線程 必須具有Outworld.published.get()== 3?

簡短的回答是no。因爲其他線程在讀取之前可能會訪問Outworld.published

在執行了Outworld.published = val;之後,在沒有對val進行其他修改的情況下 - 是的 - 它始終是3

但是,如果任何線程執行val.increment那麼其值就可能不同於其他線程。

+0

是的,我說的是我們可能認爲這個對象是有效不可變的情況;即我們完全確定沒有其他線程會對其調用「increment()」。 我更新了一個更具體的例子。 – 2012-04-22 09:20:58

4

有幾個條件必須滿足Java內存模型,以保證Outworld.published.get() == 3

  • 的你貼的代碼創建並遞增MutableLong片斷,然後設置Outworld.published場,必須發生知名度之間的步驟。實現這一點的一種方式是讓所有代碼在單個線程中運行 - 保證「as-if-serial semantics」。我認爲這就是你的意圖,但認爲值得指出。
  • Outworld.published的讀數必須有發生之後的語義。一個例子可能是執行相同的線程Outworld.published = val;然後啓動其他可以讀取值的線程。這將保證「好像串行」語義,從而防止在讀取之前重新排序讀取。

如果您能夠提供這些保證,那麼JMM將保證所有線程都能看到Outworld.published.get() == 3


但是,如果您對這方面的一般計劃設計建議感興趣,請繼續閱讀。

爲保證沒有其他線程看到Outworld.published.get()的不同值,您(開發人員)必須保證您的程序不會以任何方式修改該值。通過隨後執行Outworld.published = differentVal;Outworld.published.increment();。雖然這是可以保證的,但如果設計代碼以避免可變對象,並使用靜態非最終字段作爲多線程的全局訪問點,可以更容易:

  • 改爲發佈MutableLong,將相關值複製到不同類的新實例中,其狀態不能修改。例如:引入類別ImmutableLong,其將value分配給final施工領域,並且沒有increment()方法。
  • 而不是多個線程訪問靜態非最終字段,將該對象作爲參數傳遞給您的實現。這將防止一個流氓線程重新分配值並干擾其他線程的可能性,並且比靜態字段重新分配更容易推理。 (不可否認,如果你正在處理遺留代碼,說起來容易做起來難)。
+0

我完全同意你的意見。同步太容易出錯。我會不惜一切代價避免它。共享可變性不是設計算法或程序的安全方式。我認爲每個程序員都應該投入一些時間來研究函數式編程概念,並將它們應用到oo世界。這絕對是值得的。 – bennidi 2012-04-22 09:33:26