簡體   English   中英

Java volatile 重新排序預防范圍

[英]Java volatile reordering prevention scope

對 volatile 字段的寫入和讀取分別防止在 volatile 字段之前和之后重新排序讀/寫。 寫入 volatile 變量之前的變量讀/寫不能重新排序在它之后發生,而從 volatile 變量讀取之后的讀/寫不能重新排序在它之前發生。 但這項禁令的范圍是什么? 據我了解, volatile 變量只能防止在使用它的塊內重新排序,對嗎?

為了清楚起見,讓我舉一個具體的例子。 假設我們有這樣的代碼:

int i,j,k;
volatile int l;
boolean flag = true;

void someMethod() {
    int i = 1;
    if (flag) {
        j = 2;
    }
    if (flag) {
        k = 3;
        l = 4;
    }
}

顯然,寫入l會阻止寫入k的重新排序,但它會阻止寫入ij相對於l重新排序嗎? 換句話說,可以在寫入l之后寫入ij嗎?

更新 1

感謝大家花時間回答我的問題 - 我很感激。 問題是你回答了錯誤的問題。 我的問題是關於范圍,而不是關於基本概念。 問題基本上是編譯器在代碼中保證“發生在之前”與 volatile 字段的關系有多遠。 顯然編譯器可以保證在同一個代碼塊內,但是封閉塊和對等塊呢 - 這就是我的問題。 @Stephen C 說,volatile 保證發生在整個方法主體內的行為之前,即使在封閉塊中,但我找不到任何確認。 他說得對嗎,有什么地方可以確認嗎?

讓我再舉一個關於范圍界定的具體例子來澄清事情:

setVolatile() {
   l = 5;
} 

callTheSet() {
   i = 6;
   setVolatile();
}

在這種情況下,編譯器會禁止對i寫的重新排序嗎? 或者編譯器不能/沒有被編程來跟蹤在 volatile 情況下其他方法中發生的事情, i寫的可以重新排序在setVolatile()之前發生? 或者編譯器根本不重新排序方法調用?

我的意思是某處必須有一個點,當編譯器將無法跟蹤某些代碼是否應該在某些易失性字段寫入之前發生時。 否則,一個易失性字段寫入/讀取可能會影響一半程序的順序,如果不是更多的話。 這是一種罕見的情況,但它是可能的。

此外,看看這個報價

在新的內存模型下,volatile 變量不能相互重新排序仍然是正確的。 不同之處在於現在不再那么容易對它們周圍的正常字段訪問重新排序。

“在他們旁邊”。 這句話暗示,有一個范圍,其中 volatile 字段可以防止重新排序。

顯然,寫入 l 會阻止寫入 k 的重新排序,但是它會阻止寫入 i 和 j 的重新排序嗎?

重新排序的含義並不完全清楚; 看我上面的評論。

但是,在 Java 5+ 內存模型中,我們可以說在寫入l之前發生的對ij的寫入在讀取l之后對另一個線程可見……前提是在 write 之后沒有寫入ijl

這確實具有限制寫入ij的指令的任何重新排序的效果。 具體來說,它們不能在寫入l之后的內存寫入屏障之后移動到,因為這可能導致它們對第二個線程不可見。

但這項禁令的范圍是什么?

本身沒有禁令。

您需要了解指令、重新排序和內存屏障只是實現 Java 內存模型的特定方式的細節。 該模型實際上是根據保證在任何“格式良好的執行”中可見的內容來定義的。

據我了解, volatile 會阻止在使用它的塊內重新排序,對嗎?

實際上,沒有。 塊不考慮。 重要的是方法中語句的(程序源代碼)順序。


@Stephen C 說,volatile 保證發生在整個方法主體內的行為之前,即使在封閉塊中,但我找不到任何確認。

確認是 JLS 17.4.3 它聲明如下:

在每個線程 t 執行的所有線程間動作中,t 的程序順序是一個總順序,它反映了根據 t 的線程內語義執行這些動作的順序。

如果所有動作以與程序順序一致的總順序(執行順序)發生,則一組動作是順序一致的,此外,變量 v 的每次讀取 r 都會看到寫入 w 寫入 v 的值,使得:

  • w 在執行順序中排在 r 之前,並且

  • 在執行順序中,沒有其他寫入 w' 使得 w 在 w' 之前並且 w' 在 r 之前。

順序一致性是對程序執行中的可見性和順序的非常有力的保證。 在順序一致的執行中,所有單獨的動作(例如讀和寫)都有一個與程序順序一致的總順序,每個單獨的動作都是原子的,對每個線程都是立即可見的。

如果一個程序沒有數據競爭,那么程序的所有執行都將看起來是順序一致的。

請注意,此定義中沒有提及塊或范圍。

編輯 2

volatile ONLY gaurentee the happens-before relation

為什么它在單線程中重新排序

考慮到我們有兩個字段:

int i = 0;
int j = 0;

我們有一種方法來編寫它們

void write() {
  i = 1;
  j = 2;
}

如您所知,編譯器可能會對它們重新排序。 那是因為編譯器認為先訪問哪個都無所謂。 因為在單線程中,它們“一起發生”。

為什么不能在多線程中重新排序

但是現在我們有另一種方法可以在另一個線程中讀取它們:

void read() {
  if(j==2) {
    assert i==1;
  }
}

如果編譯器仍然對其重新排序,則此斷言可能會失敗。 這意味着j一直是2 ,但i出乎意料地不是1 似乎i=1發生在assert i==1

什么volatile

volatile只保證the happens-before relation

現在我們添加volatile

volatile int j = 0;

當我們觀察到j==2為真時,這意味着j=2發生了並且i=2在它之前,它必須發生。 所以斷言現在永遠不會失敗。

“防止重新排序”只是編譯器提供這種保證的一種方法。

結論

你現在唯一應該做的事情是happens-before 請參考下面的java規范鏈接。 reordering or not只是此保證的side effect

回答你的問題

由於lvolatile ,所以在someMethod訪問l之前總是先訪問ij 事實是,在l=4行之前的每一件事都會在它之前發生。

編輯 1

由於帖子已被編輯。 這里進一步說明。

對 volatile 字段(第 8.3.1.4 節)的寫入發生在該字段的每次后續讀取之前。

happens-before意思是:

如果一個動作發生在另一個之前,那么第一個動作對第二個動作可見並在第二個動作之前排序。

所以對ij的訪問發生在對l訪問之前。

參考: https : //docs.oracle.com/javase/specs/jls/se10/html/jls-17.html#jls-17.4.5

原答案

不, volatile只能保護自己,盡管在volatile附近重新排序字段訪問並不容易。

在新的內存模型下,volatile 變量不能相互重新排序仍然是正確的。 不同之處在於現在不再那么容易對它們周圍的正常字段訪問重新排序。 寫入易失性字段與監視器釋放具有相同的記憶效應,而從易失性字段讀取與監視器獲取具有相同的記憶效應。 實際上,由於新的內存模型對 volatile 字段訪問與其他字段訪問(無論是否 volatile )的重新排序施加了更嚴格的限制,線程 A 在寫入 volatile 字段 f 時可見的任何內容在線程 B 讀取 f 時都變為可見。

volatile關鍵字僅保證:

對 volatile 字段的寫入發生在每次后續讀取同一 volatile 之前。

參考: http : //www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile

我很想知道 volatile 變量如何影響其他字段

易失性變量確實會影響其他字段。 如果 JIT 編譯器認為重新排序不會對執行輸出產生任何影響,則可以對指令重新排序。 因此,如果您有 6 個獨立變量存儲,則 JIT 可以重新排序指令。

但是,如果您將變量設為 volatile,即在您的情況下為變量l,那么 JIT 將不會在 volatile STORE 之后對任何變量 STORES 重新排序。 我認為這是有道理的,因為在多線程程序中,如果我將變量l的值設為 4,那么我應該將i設為 1,因為在我的程序中,是在l之前編寫的,最終是程序順序語義(如果我是沒有錯)。

Note that volatile variables does two things:
  1. 編譯器不會在易失性存儲之后重新排序任何存儲/在易失性讀取之前不會重新排序任何讀取。
  2. 刷新加載/存儲緩沖區,以便所有處理器都可以看到更改。

編輯:

好博客在這里: http : //jpbempel.blogspot.com/2013/05/volatile-and-memory-barriers.html

也許我知道你所關注的“真正的范圍”。

兩種重新排序是指令結果無序的主要原因: 1. 編譯器優化 2. Cpu 處理器記錄(主要由緩存和主存同步引起)

volatile 關鍵字首先需要確認 volatile 變量被刷新,同時其他變量也被刷新到主存。 但是由於編譯器重新排序, volatile valatile 變量之前的一些可寫指令可能會被重新排序在 volatile 變量之后,讀者可能混淆讀取程序順序中位於 volatile 變量之前的非實時其他變量值,因此制定了“強制在 volatile 變量之前運行變量之前的變量寫入指令”的規則。此優化由 Java 完成編譯器或 JIT。

重點是指令中編譯器的優化,如查找死代碼、指令重新排序操作,指令代碼范圍始終是“基本塊”(除了一些其他常量傳播優化等)。 基本塊是內部沒有jmp指令的一組指令,所以這是一個基本塊。 所以在我看來,重新排序操作在范圍基本塊中是固定的。 源代碼中的基本塊總是一個塊或方法的主體。

也因為java沒有內聯函數,方法調用是由動態調用方法指令使用的,重新排序操作不應該跨越兩個方法。

因此,范圍不會大於“方法主體”,或者可能只是“for”主體的區域,這是基本塊范圍。

這就是我的全部想法,我不確定它是否正確,有人可以幫助使其更准確。

暫無
暫無

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

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