發生在推理之前應用於一些例子

我們將提供一些示例來說明如何應用發生之前的推理來檢查寫入對後續讀取是否可見。

單執行緒程式碼

正如你所期望的那樣,寫入對於單執行緒程式中的後續讀取始終可見。

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

通過發生 - 在規則#1 之前:

  1. write(a) 動作發生在 write(b) 動作之前
  2. write(b) 動作發生在 read(a) 動作之前
  3. read(a) 動作發生在 read(a) 動作之前

通過發生 - 在規則#4 之前:

  1. write(a) 發生 - 在 write(b)write(b) 發生之前 - 在 read(a) IMPLIES write(a) 發生之前 - 發生之前 12。
  2. write(b) 發生 - 在 read(a)read(a) 發生之前 - 在 read(b) 之前發生了 write(b) 發生 - 在 read(b) 之前

加起來:

  1. write(a) 發生 - 在 read(a) 關係之前意味著 a + b 語句保證看到 a 的正確值。
  2. write(b) 發生 - 在 read(b) 關係之前意味著 a + b 語句保證看到 b 的正確值。

具有 2 個執行緒的示例中的 volatile 行為

我們將使用以下示例程式碼來探討記憶體模型對於 volatile 的一些含義。

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

首先,考慮涉及 2 個執行緒的以下語句序列:

  1. 建立了一個 VolatileExample 例項; 叫它 ve
  2. ve.update(1, 2) 在一個執行緒中被呼叫,並且
  3. ve.observe() 在另一個執行緒中被呼叫。

通過發生 - 在規則#1 之前:

  1. write(a) 動作發生在 volatile-write(a) 動作之前
  2. volatile-read(a) 動作發生在 read(b) 動作之前

通過發生 - 在規則#2 之前:

  1. 第一個執行緒中的 volatile-write(a) 動作發生在第二個執行緒中的 volatile-read(a) 動作之前

通過發生 - 在規則#4 之前:

  1. 第一個執行緒中的 write(b) 動作發生在第二個執行緒中的 read(b) 動作之前

換句話說,對於這個特定的序列,我們保證第二個執行緒將看到第一個執行緒對非易失性變數 b 的更新。但是,還應該清楚的是,如果 update 方法中的賦值是相反的,或者 observe() 方法在 a 之前讀取變數 b,那麼之前發生的鏈將被破壞。如果第二個執行緒中的 volatile-read(a) 不在第一個執行緒中的 volatile-write(a) 之後,鏈也會被破壞。

當鏈斷裂時,無法保證 observe() 能看到 b 的正確值。

揮發性有三個執行緒

假設我們在前面的例子中新增第三個執行緒:

  1. 建立了一個 VolatileExample 例項; 叫它 ve
  2. 兩個執行緒稱為 update
    • ve.update(1, 2) 在一個執行緒中被呼叫,
    • 在第二個帖子中呼叫 ve.update(3, 4)
  3. 隨後在第三個執行緒中呼叫 ve.observe()

要完全分析這一點,我們需要考慮第一個執行緒和第二個執行緒中語句的所有可能交錯。相反,我們只考慮其中兩個。

場景#1 - 假設 update(1, 2)update(3,4) 之前我們得到這個序列:

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

在這種情況下,很容易看出從 write(b, 3)read(b) 之前有一個完整的發生鏈。此外,沒有干預寫入 b。因此,對於這種情況,第三個執行緒保證看到 b 具有值 3

場景#2 - 假設 update(1, 2)update(3,4) 重疊並且交錯如下:

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

現在,雖然從 write(b, 3)read(b) 有一個發生在前的鏈,但是另一個執行緒執行了干預的 write(b, 1) 動作。這意味著我們無法確定 read(b) 會看到哪個值。

(旁白:這表明我們不能依賴 volatile 來確保非易失性變數的可見性,除非在非常有限的情況下。)