使用 synchronized volatile 時視覺化讀寫障礙

我們知道我們應該使用 synchronized 關鍵字來執行方法或塊獨佔。但是我們很少有人可能沒有意識到使用 synchronizedvolatile 關鍵字的另一個重要方面: 除了使程式碼單元成為原子之外,它還提供了讀/寫屏障。這個讀/寫障礙是什麼?讓我們用一個例子討論這個:

class Counter {

  private Integer count = 10;

  public synchronized void incrementCount() {
    count++;
  }

  public Integer getCount() {
    return count;
  }
}

讓我們假設一個執行緒 A 先呼叫 incrementCount() 然後另一個執行緒 B 呼叫 getCount()。在這種情況下,無法保證 B 將看到 count 的更新值。它仍然可以看到 count 作為 10,即使它也可能永遠不會看到 count 的更新價值。

要理解這種行為,我們需要了解 Java 記憶體模型如何與硬體架構整合。在 Java 中,每個執行緒都有自己的執行緒堆疊。該堆疊包含:方法呼叫堆疊和在該執行緒中建立的區域性變數。在多核系統中,兩個執行緒很可能在不同的核心中併發執行。在這種情況下,執行緒堆疊的一部分可能位於核心的暫存器/快取中。如果在一個執行緒內,使用 synchronized(或 volatile)關鍵字訪問一個物件,則在 synchronized 塊之後該執行緒將該變數的本地副本與主記憶體同步。這會建立一個讀/寫屏障,並確保執行緒正在檢視該物件的最新值。

但在我們的例子中,由於執行緒 B 沒有使用對 count 的同步訪問,它可能是指儲存在暫存器中的 count 的值,並且可能永遠不會看到來自執行緒 A 的更新。為了確保 B 看到計數的最新值,我們需要使用 getCount() 同步也是如此。

public synchronized Integer getCount() {
  return count;
}

現在,當執行緒 A 完成更新 count 時,它會解鎖 Counter 例項,同時建立寫屏障並將該塊內完成的所有更改重新整理到主儲存器。類似地,當執行緒 B 獲取對 Counter 的同一例項的鎖定時,它進入讀屏障並從主儲存器讀取 count 的值並檢視所有更新。

StackOverflow 文件

同樣的可見性效果也適用於 volatile 讀/寫。在寫入 volatile 之前更新的所有變數將被重新整理到主儲存器,並且在 volatile 變數讀取之後的所有讀取都將來自主儲存器。