volatile 關鍵字的工作機制

ACatSmiling發表於2024-10-02

Author: ACatSmiling

Since: 2024-07-24

volatile 關鍵字是 Java 程式語言中的一個重要工具,用於控制變數在多執行緒環境中的可見性和有序性。

前置知識

指令重排序

指令重排序(Instruction Reordering):是現代處理器和編譯器最佳化技術的一部分,旨在提高程式執行效率。透過改變指令的執行順序,可以更好地利用處理器流水線和快取,從而提升效能。然而,在多執行緒環境中,指令重排序可能引發執行緒安全問題,因為它可能改變程式的預期執行順序。

指令重排序的型別:

  1. 編譯器重排序(Compiler Reordering):編譯器在生成機器程式碼時,可能會根據依賴關係和最佳化策略對程式碼指令進行重排序。
  2. 處理器重排序(Processor Reordering):處理器在執行指令時,為了提高流水線效率和快取利用率,可能會對指令進行重排序。處理器有專門的機制,如亂序執行(Out-of-Order Execution)和記憶體模型(Memory Model),來管理指令的執行順序。

記憶體屏障

記憶體屏障(Memory Barrier)有時也稱為記憶體柵欄(Memory Fence),是一種用於控制處理器和編譯器在多執行緒環境中指令執行順序的機制。記憶體屏障透過防止某些型別的指令重排序,確保特定的記憶體操作在程式中按照預期的順序執行。

記憶體屏障的作用:

  1. 防止指令重排序:記憶體屏障強制指令在特定的順序執行,防止編譯器或處理器對其進行重排序。
  2. 確保記憶體可見性:記憶體屏障確保某些記憶體操作的結果對其他執行緒立即可見。特別是在多處理器環境中,記憶體屏障可以強制處理器將快取的資料重新整理到主記憶體,或者從主記憶體重新讀取資料。

記憶體屏障主要有以下幾種型別:

  1. Load Barrier(讀屏障):確保屏障之前的所有讀操作在屏障之後的讀操作之前完成。
  2. Store Barrier(寫屏障):確保屏障之前的所有寫操作在屏障之後的寫操作之前完成。
  3. Full Barrier(全屏障):同時具備讀屏障和寫屏障的功能,確保屏障之前的所有讀寫操作在屏障之後的讀寫操作之前完成。

在 Java 中,記憶體屏障主要透過 volatile 關鍵字和 synchronized 關鍵字得以應用:

  • volatile 關鍵字
    1. 讀 volatile 變數時,會插入讀屏障:確保在屏障之後的所有讀操作都能看到 volatile 變數的最新值。
    2. 寫 volatile 變數時,會插入寫屏障:確保在屏障之前的所有寫操作都已經完成並對其他執行緒可見。
  • synchronized 關鍵字:
    1. 進入 synchronized 塊時,會插入讀屏障:確保在進入臨界區之前,所有的讀操作都能看到最新的值。
    2. 退出 synchronized 塊時,會插入寫屏障:確保在退出臨界區之前,所有的寫操作都已經完成並對其他執行緒可見。

CPU 快取一致性協議

CPU 快取一致性協議(Cache Coherence Protocol)是用於確保在多處理器系統中,各個處理器的快取內容保持一致的機制。當多個處理器共享同一塊記憶體時,它們可能會在各自的快取中儲存該記憶體的副本。為了確保這些副本始終一致,快取一致性協議被引入。

快取一致性問題:在多處理器系統中,如果一個處理器修改了某個記憶體位置的值,而其他處理器快取中仍然保留舊值,則會導致資料不一致的問題。這種情況在併發程式設計中非常常見,解決這一問題需要快取一致性協議。

常見的快取一致性協議

快取一致性協議是多處理器系統中確保資料一致性的關鍵機制。透過定義快取行的多種狀態和相應的轉換規則,快取一致性協議有效地解決了資料不一致的問題。常見的協議包括 MSI、MESI、MOESI 和 MESIF,它們在不同的應用場景中各有優劣。

MSI 協議:MSI 是最簡單的一種快取一致性協議,它的名字來源於三種快取行狀態:

  • Modified(修改):快取行的資料已被修改,且資料只在當前快取中是最新的,主記憶體中的資料已過期。
  • Shared(共享):快取行的資料可能被多個快取共享,且資料與主記憶體中的一致。
  • Invalid(無效):快取行的資料無效,需要從主記憶體或其他快取獲取最新資料。

MESI 協議:MESI 協議是 MSI 協議的擴充套件,多了一種狀態:

  • Exclusive(獨佔):快取行的資料是最新的,且只有當前快取持有這個資料,主記憶體中的資料也是最新的。

MOESI 協議:MOESI 協議是在 MESI 協議基礎上再擴充套件了一種狀態:

  • Owner(擁有者):快取行的資料是最新的,且資料可能被其他快取共享,但當前快取是資料的擁有者,負責向其他快取提供資料。

MESIF 協議:MESIF 協議是英特爾的一種快取一致性協議,增加了一種狀態:

  • Forward(轉發者):類似於 Shared 狀態,但在多個快取行都處於 Shared 狀態時,Forward 狀態的快取行負責向其他請求者提供資料,減少了對主記憶體的訪問。

快取一致性協議的工作機制

快取一致性協議通常採用以下兩種機制來保證快取的一致性:

  1. 匯流排嗅探(Bus Snooping):每個處理器的快取控制器都會監聽(嗅探)匯流排上其他處理器的讀寫操作,如果發現某個快取行被修改,就會相應地更新或失效本地快取中的資料。
  2. 目錄協議(Directory Protocol):維護一個全域性目錄,記錄每個快取行在哪些處理器的快取中。處理器的讀寫操作需要查詢和更新這個全域性目錄,以確保一致性。

快取一致性協議的實現示例

以下是一個簡化的 MESI 協議工作示例。

假設有兩個處理器 P1 和 P2,它們都快取了記憶體地址 X 的資料:

  1. 初始狀態:P1 和 P2 的快取行狀態都為 Shared。
  2. P1 修改 X:P1 將快取行狀態修改為 Modified,同時透過匯流排嗅探通知 P2 失效其快取行,P2 的快取行狀態變為 Invalid。
  3. P2 讀取 X:P2 發現其快取行已失效,從 P1 或主記憶體獲取最新資料,P1 的快取行狀態變為 Owner,P2 的快取行狀態變為 Shared。

volatile 的作用

volatile 主要有兩個作用:

  1. 保證可見性當一個執行緒修改了 volatile 變數的值,新值會被立即重新整理到主記憶體中,其他執行緒讀取該變數時會直接從主記憶體中讀取最新的值。
    • 在沒有 volatile 的情況下,一個執行緒對變數的修改可能不會立即被其他執行緒看到,因為每個執行緒可能會在自己的工作記憶體(快取)中操作變數的副本。
    • 使用 volatile 關鍵字時,任何對該變數的寫操作都會立即重新整理到主記憶體,並且任何讀操作都會直接從主記憶體中讀取。這樣就確保了變數的最新值對所有執行緒可見。
  2. 禁止指令重排序對 volatile 變數的讀寫操作不會被編譯器和處理器重排序,這保證了操作的有序性。
    • volatile 禁止了指令重排序最佳化。通常,編譯器和處理器為了提高效能,可能會對指令進行重排序,但這種重排序會帶來執行緒安全問題。
    • 使用 volatile 後,編譯器和處理器會在讀寫 volatile 變數時插入記憶體屏障(Memory Barrier),確保在記憶體屏障前的操作不會被重排序到屏障之後,反之亦然。

volatile 無法保證原子性。

volatile 的工作原理

  1. 記憶體屏障(Memory Barrier)
    • 在寫入 volatile 變數時,會插入寫屏障(Store Barrier),確保在屏障之前的所有寫操作都被重新整理到主記憶體。
    • 在讀取 volatile 變數時,會插入讀屏障(Load Barrier),確保在屏障之後的所有讀操作都從主記憶體中讀取最新的值。
    • 三句話說明:
      1. volatile 寫之前的操作,都禁止重排序到 volatile 之後。
      2. volatile 讀之後的操作,都禁止重排序到 volatile 之前。
      3. volatile 寫之後的 volatile 讀,禁止重排序。
  2. CPU 快取一致性協議:volatile 變數的寫操作會觸發快取一致性協議,強制其他處理器的快取行失效,從而確保所有處理器都能看到變數的最新值。

volatile 的應用場景

狀態標誌:適用於簡單的狀態標誌或開關,例如一個布林值,用於控制執行緒是否繼續執行。

private volatile boolean running = true;

public void stop() {
    running = false;
}

public void run() {
    while (running) {
        // 執行任務
    }
}

雙重檢查鎖定(Double-Checked Locking):用於實現執行緒安全的單例模式。

public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
	}
}

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/JavaLanguage/java-util-concurrent.md

相關文章