簡體   English   中英

了解 Java 易失性可見性

[英]Understanding Java volatile visibility

我正在閱讀有關 Java volatile關鍵字的信息,但對其“可見性”感到困惑。

volatile 關鍵字的典型用法是:

volatile boolean ready = false;
int value = 0;

void publisher() {
    value = 5;
    ready = true;
}

void subscriber() {
    while (!ready) {}
    System.out.println(value);
}

正如大多數教程所解釋的,使用 volatile for ready可確保:

  • 在發布者線程上更改為ready對其他線程(訂閱者)立即可見;
  • ready的變化是對其他線程可見,前述的任何變量更新ready (這里是value的變化)也是其他線程可見的;

我理解第二點,因為volatile變量通過使用內存屏障來防止內存重新排序,所以在 volatile write 之前的寫入不能在它之后重新排序,並且在 volatile read 之后的讀取不能在它之前重新排序。 這就是ready防止上面演示中打印value = 0 的方式。

但是我對第一個保證感到困惑,即 volatile 變量本身的可見性。 對我來說,這聽起來是一個非常模糊的定義。

換句話說,我的困惑僅在於 SINGLE 變量的可見性,而不是多個變量的重新排序或其他什么。 讓我們簡化上面的例子:

volatile boolean ready = false;

void publisher() {
    ready = true;
}

void subscriber() {
    while (!ready) {}
}

如果ready沒有定義為 volatile,那么訂閱者是否有可能無限地卡在 while 循環中? 為什么?

我想問幾個問題:

  • “立即可見”是什么意思? 寫操作需要一些時間,那么其他線程多久可以看到 volatile 的變化呢? 在寫入開始后不久但在寫入完成之前發生的另一個線程中的讀取是否可以看到更改?
  • 可見性,對於現代 CPU 來說,緩存一致性協議(例如 MESI)無論如何都可以保證,那么volatile在這里有什么幫助呢?
  • 有些文章說 volatile 變量直接使用內存而不是 CPU 緩存,這保證了線程之間的可見性。 這聽起來不是一個正確的解釋。
   Time : ---------------------------------------------------------->

 writer : --------- | write | -----------------------
reader1 : ------------- | read | -------------------- can I see the change?
reader2 : --------------------| read | -------------- can I see the change?

希望我清楚地解釋了我的問題。

語言規范的相關部分:

volatile 關鍵字: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-8.3.1.4

內存模型: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-17.html#jls-17.4

正如您所說,CPU 緩存不是這里的一個因素。

這更多是關於優化。 如果ready不是 volatile,則編譯器可以自由解釋

// this
while (!ready) {}

// as this
if (!ready) while(true) {}

這當然是一種優化,它必須更少地評估條件。 該值在循環中不會改變,可以“重用”。 就單線程語義而言,它是等效的,但它不會做你想要的。

這並不是說這會一直發生。 編譯器可以自由地這樣做,他們不必這樣做。

可見性,對於現代 CPU 來說,緩存一致性協議(例如 MESI)無論如何都可以保證,那么 volatile 在這里有什么幫助呢?

那對你沒有幫助。 您不是在為現代 CPU 編寫代碼,而是在為 Java 虛擬機編寫代碼,該虛擬機允許具有虛擬 CPU 的虛擬機的虛擬 CPU 緩存不一致。

有些文章說 volatile 變量直接使用內存而不是 CPU 緩存,這保證了線程之間的可見性。 這聽起來不是一個正確的解釋。

那是正確的。 但是請理解,這與您正在編碼的虛擬機有關。 它的內存很可能在您的物理 CPU 的緩存中實現。 這可能允許您的機器使用緩存並仍然具有 Java 規范所需的內存可見性。

使用volatile可以確保寫入直接進入虛擬機的內存,而不是虛擬機的虛擬 CPU 緩存。 虛擬機的 CPU 緩存不需要提供線程之間的可見性,因為 Java 規范不需要它。

您不能假設您的特定物理硬件的特性一定會提供 Java 代碼可以直接使用的好處。 相反,JVM 會犧牲這些好處來提高性能。 但這意味着您的 Java 代碼無法獲得這些好處。

同樣,您不是為物理 CPU編寫代碼,而是為 JVM 提供的虛擬 CPU 編寫代碼。 那你的CPU有連貫的高速緩存允許JVM做各種能夠提升你的代碼的性能優化,但要求JVM通過對你的代碼和真正的JVM的通過那些連貫的緩存沒有。 這樣做將意味着消除大量極其有價值的優化。

如果 ready 沒有定義為 volatile,那么訂閱者是否有可能無限地卡在 while 循環中?

是的。

為什么?

因為訂閱者可能永遠不會看到發布者寫入的結果。

因為...... JLS不需要將變量的值寫入內存......除非滿足指定的可見性約束。

“立即可見”是什么意思? 寫操作需要一些時間,那么其他線程多久能看到 volatile 的變化呢? 在寫入開始后不久但在寫入完成之前發生的另一個線程中的讀取是否可以看到更改?

(我認為)JMM 指定或假設在物理上不可能同時讀取和寫入相同的概念內存單元。 因此,對存儲單元的操作是按時間順序進行的。 立即可見意味着在寫入后的下一個可能的讀取機會中可見。

可見性,對於現代 CPU 來說,緩存一致性協議(例如 MESI)無論如何都可以保證,那么 volatile 在這里有什么幫助呢?

  1. 編譯器通常生成將變量保存在寄存器中的代碼,並且僅在必要時將值寫入內存。 將變量聲明為volatile意味着該值必須寫入內存。 如果考慮到這一點,則不能依賴緩存實現的(假設或實際)行為來指定volatile含義。

  2. 雖然當前這一代現代 CPU / 緩存架構的行為方式如此,但並不能保證所有未來的計算機都會如此行為。

有些文章說 volatile 變量直接使用內存而不是 CPU 緩存,這保證了線程之間的可見性。

有人說這是不正確的……對於實現緩存一致性協議的 CPU。 然而,這不是重點,因為如上所述,變量的當前值可能尚未寫入緩存。 實際上,它可能永遠不會寫入緩存。

 Time : ----------------------------------------------------------> writer : --------- | write | ----------------------- reader1 : ------------- | read | -------------------- can I see the change? reader2 : --------------------| read | -------------- can I see the change?

因此,讓我們假設您的圖表顯示物理時間並表示在不同物理內核上運行的線程,通過它們各自的緩存讀取和寫入緩存一致的內存單元。

在物理層會發生什么取決於緩存一致性是如何實現的。

希望Reader 1 可以看到單元格的先前狀態(如果它可以從其緩存中獲得)或新狀態(如果不是)。 讀者 2 將看到新狀態。 但這也取決於寫入線程的緩存失效傳播到其他緩存所需的時間。 以及其他各種難以解釋的東西。

簡而言之,我們真的不知道在物理層面會發生什么。

但另一方面,上圖中的作者和讀者無論如何都無法真正觀察到物理時間。 程序員也不能。

程序/程序員看到的是讀寫不重疊。 當必要的發生在關系存在之前,將保證一個線程的內存寫入對另一個線程的后續1讀取的可見性。 這適用於 volatile 變量,以及其他各種事物。

如何實施保證,不是你的問題。 如果您確實了解它在硬件級別上發生的事情,那真的無濟於事,因為您實際上並不知道 JIT 編譯器將發出什么代碼(今天!)。


1 - 也就是說,根據同步順序隨后......您可以將其視為邏輯時間。 JLS 內存模型實際上根本不談論時間。

回答你的3個問題:

  1. 易失性寫入的更改不需要對易失性負載“立即”可見。 正確同步的 Java 程序將表現得好像它是順序一致的,並且對於順序一致性,加載/存儲的實時順序是不相關的。 因此只要不違反程序順序(或只要沒有人可以觀察到它),讀取和寫入就可以傾斜。 線性化 = 順序一致性 + 尊重實時順序。 有關更多信息,請參閱此答案

  2. 我仍然需要深入研究可見的確切含義,但 AFAIK 主要是編譯器級別的問題,因為硬件將無限期地阻止緩沖加載/存儲。

  3. 你是完全正確的文章是錯誤的。 寫了很多廢話,“將易失性寫入刷新到主內存而不是使用緩存”是我看到的最常見的誤解。 我認為我所有的 SO 評論中有 50% 是關於告訴人們緩存總是連貫的。 關於該主題的一本好書是“內存一致性和緩存一致性入門 2e”,可免費獲得

Java 內存模型的非正式語義包含 3 個部分:

  • 原子性
  • 能見度
  • 訂購

原子性是關於確保讀/寫/rmw 在全局內存順序中原子地發生。 所以沒有人可以觀察到一些介於兩者之間的狀態。 這涉及訪問原子性,如讀/寫撕裂、字撕裂和正確對齊。 它還處理像 rmw 這樣的操作原子性。

恕我直言,它還應該處理存儲原子性; 因此,請確保有一個時間點,所有核心都可以看到存儲。 例如,如果您有 X86,那么由於負載緩沖,存儲可以比其他內核更早地對發布內核可見,並且您違反了原子性。 但我還沒有看到 JMM 中提到它。

可見性:這主要是為了防止編譯器優化,因為硬件將無限期地防止延遲加載和緩沖存儲。 在一些文獻中,他們還會在可見性下對周圍的負載/商店進行排序; 但我不相信這是正確的。

排序:這是內存模型的基礎。 它將確保單個處理器發出的加載/存儲不會被重新排序。 在第一個示例中,您可以看到這種行為的必要性。 這是編譯器屏障和cpu內存屏障的領域。

有關更多信息,請參閱: https : //download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/

我只談這部分:

在發布者線程上更改為就緒對其他線程立即可見

這是不正確的,文章是錯誤的。 文檔在這里做了一個非常明確的聲明:

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

這里比較復雜的部分是后續 通俗地說,這意味着當有人將ready視為true ,它也會將value視為5 這自然意味着您需要觀察該值是否為true ,並且您可能會觀察到不同的事情。 所以這不是“立即”。

人們對此感到困惑的是, volatile提供了順序一致性,這意味着如果有人觀察到ready == true ,那么每個人也會(例如,與release/acquire不同)。

暫無
暫無

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

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