2016-02-03 21 views
2

這是JCiP的一個例子。不可變的對象是否可以免於不適當的發佈?

public class Unsafe { 
    // Unsafe publication 
    public Holder holder; 

    public void initialize() { 
     holder = new Holder(42); 
    } 
} 

public class Holder { 
    private int n; 

    public Holder(int n) { 
     this.n = n; 
    } 
    public void assertSanity() { 
     if (n != n) { 
      throw new AssertionError("This statement is false."); 
     } 
    } 
} 

在第34頁:

[15]這裏的問題不是Holder類本身,而是 持有人不能正常出版。然而,通過聲明n字段是最終的,持有者可以被免疫 以不適當的公佈,其中 將使得Holder不可變;

而且從this answer

最終規範(見@ andersoj的答案)保證 當構造函數返回時,最後一個字段會一直正確 (從所有線程中可見)初始化。

wiki

例如,在Java中,如果一個構造函數的調用具有 被內聯,則共享變量可能會立即更新一次 存儲已經撥出而內聯構造函數之前 初始化對象

我的問題是:

因爲:(可能是錯誤的,我不知道)

a)共享變量可能會在內聯構造函數初始化對象之前立即更新。

b)只有當構造函數返回時,最終字段才能保證正確初始化(如所有線程都可見)。

是否有可能其他線程看到默認值holder.n? (即另一個線程在holder構造函數返回之前獲得對holder的引用。)

如果是這樣,那麼如何解釋下面的語句? 從JCiP:

持有人可以通過聲明n個 場是決賽,這將使持有人不變

編輯進行免疫出版不當。不可變對象的定義:

一個目的是不可變的,如果:
X及其狀態不能之後 結構進行修改;

X中的所有其字段是最後; [12]和

X是適當地構成(在該參考文獻沒有施工期間逸出) 。

因此,根據定義,不可變對象沒有「this引用轉義」問題。對?

但是,如果沒有聲明爲volatile,他們會遭受Out-of-order writes雙重檢查鎖定模式?

+0

Java內存模型保證所有不可變對象的安全發佈,而不顯示同步,所有不可變對象的實例字段聲明爲final。 – scottb

+0

@scottb錯誤。構造函數本身可以泄漏參考。 – chrylis

回答

4

一個不可變的對象,例如String,似乎對所有讀者來說都是相同的狀態,不管其參考如何獲得,即使是在不正確的同步和缺少發生關係之前。

這是由在Java中5.數據訪問引入final字段的語義通過最終領域取得了一個更強的存儲器語義,如在jls-17.5.1

定義。在重新排序的編譯器的術語和存儲器障礙,有更多的限制時處理最終字段,請參閱JSR-133 Cookbook。你擔心的重新排序不會發生。

是的 - 雙重檢查鎖定可以通過包裝中的最後一個字段來完成;沒有volatile是必需的!但是這種方法不一定更快,因爲需要兩次讀取。


請注意,這種語義適用於單個最終字段,而不是整個對象。例如,String包含可變字段hash;不過,String被認爲是不可變的,因爲它的公共行爲僅基於final字段。

最後一個字段可以指向一個可變對象。例如,String.valuechar[],它是可變的。要求不可變對象是最終字段的樹是不切實際的。

final char[] value; 

public String(args) { 
    this.value = createFrom(args); 
} 

只要我們不修改的value構造函數退出後的內容,它的罰款。

我們可以按任意順序修改構造函數中的value的內容,這沒關係。

public String(args) { 
    this.value = new char[1]; 
    this.value[0] = 'x'; // modify after the field is assigned. 
} 

又如

final Map map; 
List list; 

public Foo() 
{ 
    map = new HashMap(); 
    list = listOf("etc", "etc", "etc"); 
    map.put("etc", list) 
} 

任何訪問通過最後字段將顯示爲不可變的,例如foo.map.get("etc").get(2)

訪問不是通過最終字段不 - foo.list.get(2)是不安全的通過不正確的發佈,即使它讀取相同的目的地。


這些都是設計動機。現在讓我們來看看JLS如何正式化它在jls-17.5.1

A freeze行動是在構造函數出口定義的,與最終字段的賦值相同。這使我們可以在構造函數內的任何地方寫入內容來填充內部狀態。

不安全刊物的常見問題是缺少發生之前(hb)的關係。即使讀取看到寫入,它也不會建立其他操作。但是,如果易失性讀取看到易失性寫入,則JMM會建立hb,並在多個操作之間建立順序。

final字段語義想要做同樣的事情,即使正常讀寫,即使是通過不安全的出版物。要做到這一點,一個內存鏈(mc)順序被添加到一個讀取所看到的任何寫入之間。

A deferences()命令限制語義訪問最終字段。

讓我們重溫Foo例子來看看它是如何工作

tmp = new Foo() 

    [w] write to list at index 2 

    [f] freeze at constructor exit 

shared = tmp; [a] a normal write 

// Another Thread 

foo = shared; [r0] a normal read 

if(foo!=null) // [r0] sees [a], therefore mc(a, r0) 

    map = foo.map;   [r1] reads a final field 

    map.get("etc").get(2) [r2] 

我們有

hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2) 

因此wr2可見。


從本質上講,通過Foo包裝,地圖(這是可變的本身)發佈安全不安全,雖然公佈......如果是有道理的。

我們可以使用包裝來建立最終的字段語義,然後丟棄它?像

Foo foo = new Foo(); // [w] [f] 

shared_map = foo.map; // [a] 

有趣的是,JLS包含足夠的條款來排除這種用例。我猜它被削弱了,所以允許更多的內部線程優化,即使是最後的字段也是如此。


注意,如果this被凍結行動之前泄露,最終場的語義無法保證。

但是,我們可以安全在構造函數中泄漏this後的凍結行動,構造函數鏈。

-- class Bar 

final int x; 

Bar(int x, int ignore) 
{ 
    this.x = x; // assign to final 
} // [f] freeze action on this.x 

public Bar(int x) 
{ 
    this(x, 0); 
    // [f] is reached! 
    leak(this); 
} 

這是安全的,至於x有關;在x上的凍結動作在分配了x的構造函數的存在處定義。這可能是爲了安全泄漏this而設計的。

+0

我在哪裏可以找到關於2個部分順序的更多信息(如某些書?):內存鏈'mc()'和取消引用鏈'dereferences()'?我總覺得JLS很難理解。謝謝! – du369

+0

我不知道。如果作者寫了一本關於'mc()'的書,他不會出售任何副本來恢復咖啡開銷。最後,這些形式化並不重要。我們遵循常見使用模式和經驗法則。該語言是爲此而設計的。該規範是爲實施者編寫的。 – ZhongYu

2

不,如果構造函數在返回之前泄漏對this的引用(這是發生之前 - 之前啓動的位置),則不可變對象仍可能不安全地發佈。

引用泄漏的兩種可能途徑是,如果構造函數試圖爲回調註冊新對象(比如在某個構造函數參數上作爲事件監聽器)或註冊表,或者更微妙地調用非final方法被覆蓋以執行相同的操作。

+0

我讀過關於JCiP中的「此參考轉義」部分。我的問題是:除了構造函數中的泄漏之外,還有其他方法可以「引用」出來嗎?例如,「重複檢查鎖定」問題是否被視爲「此參考轉義」的示例? – du369

+0

@ du369雙重檢查鎖定是完全不同的構造。構造函數的唯一方式是從構造函數直接(傳遞或分配'this')或間接(調用本身有權訪問'this'並泄漏它的方法)。 – chrylis

+0

從構造函數泄漏'this'對於不可變對象是特定或唯一的問題。任何獲取對未完成構造實例的引用的對象都可以以不一致或無效的狀態查看該實例,可變或不可變。假設'this'沒有泄漏,Java存儲器模型能夠推斷真正的不可變對象的安全發佈。 – scottb

相關問題