簡體   English   中英

不是線程安全的對象發布

[英]Not thread-safe Object publishing

閱讀“實踐中的Java並發性”,第3.5節中有以下部分:

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

除了創建兩個Holder實例存在明顯的線程安全隱患外,該書還聲稱可能會發生發布問題。

此外,對於Holder類,例如

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

可以引發AssertionError

這怎么可能? 我能想到的唯一允許這種荒謬行為的方法是,如果Holder構造函數不會被阻塞,那么當構造函數代碼仍在另一個線程中運行時,將創建對實例的引用。

這可能嗎?

之所以可行,是因為Java的內存模型較弱。 它不保證讀寫順序。

可以通過以下代表兩個線程的兩個代碼片段來重現此特定問題。

線程1:

someStaticVariable = new Holder(42);

線程2:

someStaticVariable.assertSanity(); // can throw

表面上看來這不可能發生。 為了理解為什么會發生這種情況,您必須超越Java語法,並降低到更低的水平。 如果您看一下線程1的代碼,則基本上可以將其分解為一系列的內存寫和分配:

  1. 分配內存到指針1
  2. 在偏移量0處將42寫入指針1
  3. 將指針1寫入someStaticVariable

因為Java的內存模型很弱,所以從線程2的角度來看,代碼完全有可能按以下順序實際執行:

  1. 分配內存到指針1
  2. 將指針1寫入someStaticVariable
  3. 在偏移量0處將42寫入指針1

害怕? 是的,但是有可能發生。

不過,這意味着線程2現在可以在n獲得值42之前調用assertSanity 。在assertSanity期間,可能兩次讀取值n ,一次在操作#3完成之前,一次在操作#3之后完成,因此看到兩個不同的值並拋出異常。

編輯

根據Jon Skeet的說法,除非字段為final,否則Java 8仍然會發生AssertionError

使用 Java的內存模型是這樣的,分配給Holder引用可能分配到對象中的變量之前變得可見。

但是,從Java 5開始生效的較新的內存模型使這成為不可能,至少對於最終字段而言:構造函數中的所有分配“在新對象對變量的引用的任何分配之前”發生。 有關更多詳細信息,請參見Java語言規范第17.4節 ,但這是最相關的代碼段:

對象的構造函數完成后,就認為該對象已完全初始化。 保證只有在對象完全初始化之后才能看到對對象的引用的線程才能保證看到該對象的最終字段的正確初始化值

因此您的示例可能仍然會失敗,因為n為非最終值,但是如果您使n最終值也可以。

當然:

if (n != n)

假設JIT編譯器沒有對其進行優化,則對於非最終變量肯定會失敗-如果操作是:

  • 提取LHS:n
  • 獲取RHS:n
  • 比較LHS和RHS

那么該值可能會在兩次提取之間發生變化。

好吧,在書中它指出了第一個代碼塊:

這里的問題不是Holder類本身,而是Holder沒有正確發布。 但是,可以通過將n字段聲明為final來使Holder免受不適當發布的影響,這將使Holder不變。 參見第3.5.2節

對於第二個代碼塊:

因為沒有使用同步來使Holder對其他線程可見,所以我們說Holder沒有正確發布。 不正確發布的對象可能會導致兩件事。 其他線程可能會在Holder字段中看到一個過時的值,因此即使將值放置在Holder中,也會看到一個空引用或其他較舊的值。 但更糟糕的是,其他線程可能會看到Holder引用的最新值,而對於Holder的狀態卻是陳舊的值。[16] 為了使事情更不可預測,線程在第一次讀取字段時可能會看到過時的值,而在下次讀取時可能會看到最新的值,這就是assertSanity可以引發AssertionError的原因。

我認為JaredPar在他的評論中幾乎已經明確了這一點。

(注意:此處不查找選票-答案允許提供比評論更詳細的信息。)

基本問題是,如果沒有適當的同步,則如何在不同的線程中體現對內存的寫入。 經典示例:

a = 1;
b = 2;

如果您在一個線程上執行此操作,則第二個線程可能會看到b設置為2,而a被設置為1。此外,第二個線程看到這些變量之一被更新與第二個線程之間可能會有無窮的時間間隔。其他變量正在更新。

從理智的角度來看這件事,如果您假設

if(n != n)

如果是原子的(我認為這是合理的,但我不確定),那么就永遠不會拋出斷言異常。

此示例位於“對包含final字段的對象的引用未逸出構造方法”下

當您使用new運算符實例化新的Holder對象時,

  1. Java虛擬機首先將在堆上分配(至少)足夠的空間以容納Holder及其超類中聲明的所有實例變量。
  2. 其次,虛擬機會將所有實例變量初始化為其默認初始值。 3.c第三,虛擬機將調用Holder類中的方法。

請參考以上內容: http : //www.artima.com/designtechniques/initializationP.html

假定:第一個線程從上午10:00開始,它通過調用new Holer(42)來調用Instiized Holder對象,1)Java虛擬機首先將在堆上分配(至少)足夠的空間來容納所有實例。在Holder及其超類中聲明的變量。 -將在10:01時間2)第二,虛擬機將所有實例變量初始化為其默認初始值-將在10:02時間3)啟動,第三,虛擬機將調用Holder類中的方法.--它將開始10:04時間

現在,Thread2在-> 10:02:01時間開始,它將調用assertSanity()10:03調用,到那時,n已初始化為默認值為零,第二個線程正在讀取過時的數據。

//不安全的出版物公共持有人;

如果您使公開的最終持有人持有人將解決此問題

要么

私人國際 如果您將private final int設為n; 將重提此問題。

請在http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html下的內容中找到:在新的JMM下,最終字段如何工作?

這個例子讓我也很困惑。 我找到了一個網站,可以對主題進行徹底的解釋,讀者可能會發現它有用: https : //www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

編輯:鏈接中的相關文本顯示:

JMM允許編譯器為新的Helper對象分配內存,並在初始化新的Helper對象之前將對該內存的引用分配給helper字段。 換句話說,編譯器可以對對helper實例字段的寫入和初始化Helper對象的寫入(即this.n = n)進行重新排序,以使前者首先出現。 這可以顯示一個競爭窗口,在此期間其他線程可以觀察到部分初始化的Helper對象實例。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM