2010-03-25 39 views
13

參考我的earlier question on incompletely constructed objects,我有第二個問題。正如Jon Skeet指出的那樣,在構造函數的最後有一個隱含的內存屏障,確保所有線程都可以看到final字段。但是如果一個構造函數調用另一個構造函數呢?在他們每個人的結尾有沒有這樣的記憶障礙,或者只有在最初被召喚的那個人的結尾?也就是說,當「錯誤」的解決方案是:鏈接構造函數時JVM的隱式內存障礙如何表現?

public class ThisEscape { 
    public ThisEscape(EventSource source) { 
     source.registerListener(
      new EventListener() { 
       public void onEvent(Event e) { 
        doSomething(e); 
       } 
      }); 
    } 
} 

和正確的人會是一個工廠方法版本:

public class SafeListener { 
    private final EventListener listener; 

    private SafeListener() { 
     listener = new EventListener() { 
      public void onEvent(Event e) { 
       doSomething(e); 
      } 
     } 
    } 

    public static SafeListener newInstance(EventSource source) { 
     SafeListener safe = new SafeListener(); 
     source.registerListener(safe.listener); 
     return safe; 
    } 
} 

將在以下工作過,或沒有?

public class MyListener { 
    private final EventListener listener; 

    private MyListener() { 
     listener = new EventListener() { 
      public void onEvent(Event e) { 
       doSomething(e); 
      } 
     } 
    } 

    public MyListener(EventSource source) { 
     this(); 
     source.register(listener); 
    } 
} 

更新:問題的關鍵是this()保證實際呼叫上面的私有構造函數(在這種情況下會有阻擋,其中預期,一切都將是安全的),或者是有可能私有構造函數將內嵌作爲優化來保存一個內存屏障(在這種情況下,直到公共構造函數結束時纔會有屏障)?

this()的規則是否在某處準確定義?如果沒有,那麼我認爲我們必須假設允許內聯鏈式構造函數,並且可能有一些JVM或者甚至可以使用它。

回答

5

我認爲它是安全的,因爲Java的內存模型指出:

Ø是一個對象,並ç構造Ø,其中,最終 場˚F寫入。最終字段上的凍結動作fo發生 當c正常或突然退出。請注意,如果一個構造函數調用另一個構造函數,並且被調用的構造函數 設置了最後一個字段,則最終字段的凍結髮生在被調用的構造函數的末尾 處。

+1

這是用權威參考備份的唯一答案。 (以下是參考文獻的實際鏈接:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html)。我唯一想知道的是,什麼*凍結*的意思在任何地方都沒有解釋過(或者至少我沒有找到它)。 – 2012-04-24 17:28:17

+1

我有同樣的問題,一個非正式的描述是在(http://www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf),更正式的定義可以在(http:// www.cs.umd.edu/~pugh/java/memoryModel/newFinal.pdf) – 2012-04-26 10:29:34

3

一個對象被認爲在其構造函數完成時被完全初始化。

這也適用於鏈式構造函數。

如果必須在構造函數中註冊,則將偵聽器定義爲靜態內部類。這是安全的。

+2

您確定編譯器沒有檢測到從另一個構造函數調用私有構造函數,然後代替實際調用,它將被內聯到調用(public)構造函數中,以保存一個內存屏障? – 2010-03-25 08:44:52

1

編輯在提示編譯器內聯私有構造函數(我沒有想到優化)的評論之後,有可能代碼是不安全的。而不安全的多線程代碼中最糟糕的部分就是似乎能夠工作,所以你最好避免完全使用它。如果你想玩不同的技巧(你真的想出於某種原因避免工廠),可以考慮添加一個包裝來保證內部實現對象中數據的一致性並註冊到外部對象中。


我的猜測是,它會脆弱,但確定。編譯器無法知道內部構造函數是否只會從其他構造函數中調用,所以它必須確保結果對於僅調用內部構造函數的代碼是正確的,因此無論使用何種機制(內存屏障?)在那裏。

我猜測編譯器會在每個構造函數的末尾添加內存屏障。問題仍然存在:在完全構建之前,您將this引用傳遞給其他代碼(可能是其他線程) - 這很糟糕,但如果剩下的唯一「構造」是註冊偵聽器,那麼對象狀態與以往一樣穩定。

的解決方案是在脆弱一些其他的一天,你或其他一些程序員可能需要另一個成員添加到該對象並可以忘記鏈構造是併發招,可能決定以初始化場公共構造函數,這樣做會增加難以檢測應用程序中潛在的數據競爭,所以我會盡量避免該構造。

順便說一句:被猜測的安全性可能是錯誤的。我不知道編譯器有多複雜/聰明,內存屏障(或類似的東西)是否可以嘗試優化掉......因爲構造函數是私有的,編譯器確實有足夠的信息來知道它是隻從其他構造函數調用,並且這是足夠的信息來確定在內部構造函數中不需要同步機制...

+0

「編譯器無法知道內部構造函數是否只會從其他構造函數中調用,所以它必須確保結果對於只調用內部構造函數的代碼纔是正確的。」 - 非常好的想法,謝謝!除非,如果編譯器檢測到內部構造函數是從另一個構造函數調用的,然後不是實際調用內部構造函數,它會將它內聯到公共的構造函數中,但是希望沒有這樣的優化:-) – 2010-03-25 08:31:33

+0

你有它:我沒有想到構造函數的* inlining *,但這是一個相當簡單的優化,大多數當前的編譯器可以實際執行。所以答案應該是:不要這樣做。 – 2010-03-25 08:59:16

1

c-tor中的轉義對象引用可以發佈未完成構造的對象。如果發佈是構造函數中的最後一條語句,即使是也是如此。

即使執行了C-tor內聯,您的SafeListener在併發環境中可能不會正常工作(我認爲這不是 - 考慮通過訪問私有C-tor使用反射創建對象)。

+0

「即使執行了c-tor內聯,您的SafeListener在併發環境中可能不會正常工作」 - 我認爲「SafeListener」將表現良好,這是應該如何根據Java Concurrency in Practice完成的。但是如果執行內聯,'MyListener'肯定不會行得通。此外,儘管私有構造函數可以通過反射或通過封閉類的其他方法「按原樣」訪問,但它並不保證該構造函數不會內聯到其他構造函數(同一類中)它。 – 2010-03-25 10:01:28

3

您的第二個版本不正確,因爲它允許'this'引用從構建過程中逃脫。有了'這個'逃避了初始化安全保證,使最終字段的安全性無效。

爲解決隱含的問題,施工結束時的障礙只發生在物體構造的最後。一個讀者提供的關於內聯的直覺是有用的;從Java內存模型的角度來看,方法邊界不存在。

+0

我認爲你的意思是*第三版本('MyListener')不正確?第二個版本來自JCIP,據說是爲了防止「這個」逃脫。但是,謝謝!因此,調用'this()'不是構造函數調用,因爲它不會使對象完全構造;該對象只在構造函數完成後才被完全構造,該構造函數首先被調用,然後返回。 – 2010-03-25 14:32:50

+2

掛上。 17.5.1(在非規範性討論中)統計「請注意,如果一個構造函數調用另一個構造函數,並且被調用的構造函數設置了最後一個字段,則最終字段的凍結髮生在被調用的構造函數的末尾。這往往表明最後一個例子是安全的。實際上做什麼是當然完全不同的事情。 – 2010-03-27 05:58:55

+0

@Tom:哇!它的確如此說。這不是官方的規格嗎?所以實現應該這樣做,否則它們會被破壞。 – 2010-03-28 08:53:12