簡體   English   中英

對於這些關於 java volatile 和重新排序的代碼,這種理解是否正確?

[英]Is this understanding correct for these code about java volatile and reordering?

根據這個重新排序規則

重新排序規則

如果我有這樣的代碼

volatile int a = 0;

boolean b = false;

foo1(){ a= 10; b = true;}

foo2(){if(b) {assert a==10;}}

讓線程 A 運行 foo1 和線程 b 運行 foo2,因為 a= 10 是一個 volatile 存儲而 b = true 是一個普通存儲,那么這兩個語句可能會被重新排序,這意味着在線程 B 中可能有 b = true 而a!=10? 那是對的嗎?

添加:

感謝您的回答!
我剛剛開始學習 Java 多線程,並且經常被關鍵字 volatile 所困擾。

許多教程都在討論 volatile 字段的可見性,就像“在對它的寫操作完成后,所有讀者(特別是其他線程)都可以看到 volatile 字段”。 我懷疑其他線程(或 CPU)如何看不到已完成的字段寫入?

據我了解,完成寫入意味着您已成功將文件寫回緩存,並且根據 MESI,如果該文件已被他們緩存,則所有其他線程都應具有無效緩存行。 一個例外(因為我對硬核不是很熟悉,所以這只是一個猜測)是結果可能會寫回寄存器而不是緩存,我不知道在這種情況下是否有一些協議來保持一致性或 volatile 使它不寫入在 java 中注冊。

在某些看起來像“隱形”的情況下會發生示例:

    A=0,B=0; 
    thread1{A=1; B=2;}  
    thread2{if(B==2) {A may be 0 here}}

假設編譯器沒有對它重新排序,我們在thread2中看到的原因是存儲緩沖區,我認為存儲緩沖區中的寫入操作並不意味着寫入完成。 由於存儲緩沖區和使隊列無效的策略,這使得對變量 A 的寫入看起來不可見,但實際上寫入操作尚未完成,而線程 2 讀取 A。即使我們使字段 B 易失,而我們將字段 B 上的寫入操作設置為帶有內存屏障的存儲緩沖區,線程 2 可以讀取帶有 0 的 b 值並完成。 對我來說, volatile 看起來不是關於它聲明的字段的可見性,而是更像是一個邊緣,以確保所有寫入發生在 ThreadA 中的 volatile 字段寫入對 volatile 字段讀取之后的所有操作可見(易失性讀取在 ThreadA 中的 volatile 字段寫入完成后發生)在另一個 ThreadB 中。

順便說一句,由於我不是母語人士,我看到可能使用我的母語的教程(還有一些英文教程)說 volatile 會指示 JVM 線程從主內存中讀取 volatile 變量的值,並且不在本地緩存它,我不認為這是真的。 我對嗎?

不管怎樣,謝謝你的回答,因為不是母語人士,我希望我表達清楚。

我很確定斷言可以觸發。 我認為易失性負載只是獲取操作( https://preshing.com/20120913/acquire-and-release-semantics/)wrt 非易失性變量,所以沒有什么能阻止加載-加載重新排序。

兩個volatile操作不能相互重新排序,但是可以在一個方向上使用非原子操作重新排序,並且您選擇了沒有保證的方向。

(注意,我不是 Java 專家;可能但不太可能volatile具有一些需要更昂貴實現的語義。)


更具體的推理是,如果斷言在轉換為某些特定體系結構的 asm 時可以觸發,則必須允許 Java 內存模型觸發。

Java volatile是(AFAIK)等價於 C++ std::atomic和默認的memory_order_seq_cst 因此foo2罐JIT編譯為ARM64與一個普通的負載b和用於獲取LDAR負載a

ldar不能在較晚的加載/存儲中重新排序,但可以與較早的時間進行重新排序。 (除了stlr發布存儲;ARM64 專門設計用於使 C++ std::atomic<> with memory_order_seq_cst / Java volatileldarstlr ,不必立即在 seq_cst 存儲上刷新存儲緩沖區,僅在看到 LDAR 時,所以該設計提供了仍然恢復 C++ 指定的順序一致性所需的最少排序(我假設是 Java)。

在許多其他 ISA 上,順序一致性存儲確實需要等待存儲緩沖區自行耗盡,因此它們實際上是有序的。 后來的非原子負載。 再次在許多 ISA 上,獲取或 SC 負載是通過正常負載完成的,然后是一個屏障,該屏障阻止負載在任一方向穿過它, 否則它們將無法工作 這就是為什么具有揮發性負載a給acquire負載指令只是做一個獲取操作的關鍵是了解如何在實踐中發生的編譯。

(在 x86 asm 中,所有加載都是獲取加載,所有存儲都是釋放存儲。不過不是順序釋放;x86 的內存模型是程序順序 + 帶有存儲轉發的存儲緩沖區,這允許 StoreLoad 重新排序,因此 Java volatile存儲需要特殊的 asm .

因此,斷言不能在 x86 上觸發,除非通過對 assignments 的編譯/JIT 時間重新排序 這是測試無鎖代碼之所以困難的一個很好的例子:失敗的測試可以證明存在問題,但在某些硬件/軟件組合上的測試無法證明正確性。)

除了 Peter Cordes 的出色回答之外,就 JMM 而言,b 上存在數據競爭,因為 b 的寫入和 b 的讀取之間的邊緣之前沒有發生,因為它是一個普通變量。 只有在邊緣存在之前發生這種情況,然后才能保證如果 b=1 的負載也看到 a=1 的負載。

你需要讓 b 變得不穩定,而不是讓 a 變得不穩定。

int a=0;
volatile int b=0;

thread1(){
    a=1
    b=1
}

thread2(){
  if(b==1) assert a==1;
}

因此,如果線程 2 看到 b=1,那么在發生之前的順序(易失性變量規則)中,此讀取將在寫入 b=1 之前進行排序。 並且由於 a=1 和 b=1 是有序發生在順序之前(程序順序規則),並且 b 的讀取和 a 的讀取在發生在順序之前(再次程序順序規則)中進行排序,那么由於發生在關系之前,在 a=1 的寫入和 a 的讀取之間有一個發生在邊緣之前; 這需要看到值 1。

您指的是使用圍欄的 JMM 的可能實現。 雖然它提供了一些關於引擎蓋下發生的事情的見解,但從圍欄的角度思考同樣具有破壞性,因為它們不是一個合適的心理模型。 請參閱以下計數器示例:

https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane

是的,斷言可能會失敗。

volatile int a = 0;

boolean b = false;

foo1(){ a= 10; b = true;}

foo2(){if(b) {assert a==10;}}

JMM 保證對volatile字段的寫入發生在從它們讀取之前 在您的示例中,任何線程 a 在a = 10之前所做的都將發生在讀取 a 之后線程 b 所做的任何事情之前(同時執行assert a == 10 )。 由於b = true在線程 a 的a = 10之后執行(對於單個線程, happens-before總是成立),因此不能保證會有排序保證。 但是,請考慮:

int a = 0;

volatile boolean b = false;

foo1(){ a= 10; b = true;}

foo2(){if(b) {assert a==10;}}

在這個例子中,情況是:

a = 10 ---> b = true---|
                       |
                       | (happens-before due to volatile's semantics)
                       |
                       |---> if(b) ---> assert a == 10

                

由於您有一個總訂單,所以斷言保證通過。

回答你的補充。

許多教程都在討論 volatile 字段的可見性,就像“在對它的寫操作完成后,所有讀者(特別是其他線程)都可以看到 volatile 字段”。 我懷疑其他線程(或 CPU)如何看不到已完成的字段寫入?

編譯器可能會弄亂代碼。

例如

boolean stop;

void run(){
  while(!stop)println();
}

第一次優化

void run(){
   boolean r1=stop;
   while(!r1)println();
}

第二次優化

void run(){
   boolean r1=stop;
   if(!r1)return;
   while(true) println();
}

所以現在很明顯這個循環永遠不會停止,因為實際上永遠不會看到要停止的新值。 對於商店,你可以做一些類似的事情,可以無限期地推遲它。

據我了解,完成寫入意味着您已成功將文件寫回緩存,並且根據 MESI,如果該文件已被他們緩存,則所有其他線程都應具有無效緩存行。

正確的。 這通常稱為“全局可見”或“全局執行”。

一個例外(因為我對硬核不是很熟悉,所以這只是一個猜測)是結果可能會寫回寄存器而不是緩存,我不知道在這種情況下是否有一些協議來保持一致性或 volatile 使它不寫入在 java 中注冊。

所有現代處理器都是加載/存儲架構(甚至是 uops 轉換后的 X86),這意味着存在顯式加載和存儲指令,可以在寄存器和內存之間傳輸數據,而像 add/sub 這樣的常規指令只能與寄存器一起使用。 所以無論如何都需要使用寄存器。 關鍵部分是編譯器應該尊重源代碼的加載/存儲並限制優化。

假設編譯器沒有對它重新排序,我們在thread2中看到的原因是存儲緩沖區,我認為存儲緩沖區中的寫入操作並不意味着寫入完成。 由於存儲緩沖區和使隊列無效的策略,這使得對變量 A 的寫入看起來不可見,但實際上在線程 2 讀取 A 時寫入操作尚未完成。

在 X86 上,存儲緩沖區中的存儲順序與程序順序一致,並將按程序順序提交到緩存。 但是在某些架構中,存儲緩沖區中的存儲可以無序提交到緩存,例如由於:

  • 寫合並

  • 允許存儲在緩存行以正確狀態返回時立即提交緩存,無論較早的是否仍在等待。

  • 與 CPU 的子集共享存儲緩沖區。

存儲緩沖區可能是重新排序的來源; 但也可能是亂序和推測性執行的一個來源。

除了存儲之外,重新排序負載也可能導致觀察存儲無序。 X86 上的負載不能重新排序,但在 ARM 上是允許的。 當然,JIT 也會把事情搞砸。

即使我們將字段 B 設置為 volatile,當我們將字段 B 上的寫操作設置為帶有內存屏障的存儲緩沖區時,線程 2 可以讀取帶有 0 的 b 值並完成。

認識到 JMM 基於順序一致性很重要; 因此,即使它是一個寬松的內存模型(將普通加載和存儲與同步操作(如易失性加載/存儲鎖定/解鎖)分開),如果程序沒有數據競爭,它也只會產生順序一致的執行。 對於順序一致性,不需要遵守實時順序。 因此,只要滿足以下條件,加載/存儲就完全沒有問題:

  1. 內存順序是所有加載/存儲的總順序

  2. 內存順序與程序順序一致

  3. 負載會在內存順序中看到它之前的最新寫入。

對我來說, volatile 看起來不是關於它聲明的字段的可見性,而是更像是一個邊緣,以確保所有寫入發生在 ThreadA 中的 volatile 字段寫入對 volatile 字段讀取之后的所有操作可見(易失性讀取在 ThreadA 中的 volatile 字段寫入完成后發生)在另一個 ThreadB 中。

你走在正確的道路上。

例子。

int a=0
volatile int b=;

thread1(){
   1:a=1
   2:b=1
}

thread2(){
   3:r1=b
   4:r2=a
}

在這種情況下,在 1-2(程序順序)之間的邊緣之前會發生一個。 如果 r1=1,則在 2-3(易失性變量)之間的邊緣之前發生,並且在 3-4(程序順序)之間的邊緣之前發生。

因為發生在關系之前是可傳遞的,所以在 1-4 之間的邊之前發生了。 所以 r2 必須是 1。

volatile 負責以下內容:

  • 可見性:需要確保加載/存儲不會得到優化。

  • 也就是說加載/存儲是原子的。 因此,不應部分看到加載/存儲。

  • 最重要的是,它需要確保保留 1-2 和 3-4 之間的順序。

順便說一句,由於我不是母語人士,我看到可能使用我的母語的教程(還有一些英文教程)說 volatile 會指示 JVM 線程從主內存中讀取 volatile 變量的值,並且不在本地緩存它,我不認為這是真的。

你是完全正確的。 這是一個非常普遍的誤解。 緩存是事實的來源,因為它們總是連貫的。 如果每次寫入都需要進入主內存,程序將變得非常緩慢。 內存只是一個溢出桶,用於存放不適合緩存的內容,並且可能與緩存完全不一致。 普通/易失性加載/存儲存儲在緩存中。 可以在特殊情況下(如 MMIO)或使用 SIMD 指令時繞過緩存,但這與這些示例無關。

不管怎樣,謝謝你的回答,因為不是母語人士,我希望我表達清楚。

這里的大多數人都不是母語人士(我當然不是)。 你的英語足夠好,你表現出很大的希望。

暫無
暫無

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

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