使用 synchronized volatile 時視覺化讀寫障礙
我們知道我們應該使用 synchronized
關鍵字來執行方法或塊獨佔。但是我們很少有人可能沒有意識到使用 synchronized
和 volatile
關鍵字的另一個重要方面: 除了使程式碼單元成為原子之外,它還提供了讀/寫屏障。這個讀/寫障礙是什麼?讓我們用一個例子討論這個:
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
的值並檢視所有更新。
同樣的可見性效果也適用於 volatile
讀/寫。在寫入 volatile
之前更新的所有變數將被重新整理到主儲存器,並且在 volatile
變數讀取之後的所有讀取都將來自主儲存器。