HOWTO · Java

Java 中的 volatile 關鍵字

本文討論了 Java 中的 volatile 關鍵字及其優缺點,並舉例說明了如何使用。

本頁內容

當多個執行緒共用一個欄位,且需要讓各執行緒讀到該欄位的最新值時,例如用來停止執行緒的旗標,可以使用 volatile。它不會讓一連串操作具備原子性。本指南將說明可見性保證、何時使用這個關鍵字就足夠,以及如何用它停止工作執行緒。

了解 volatile 的可見性

執行緒可能重複使用先前讀取的值,或以無法及時反映其他執行緒寫入的方式保留該值。將欄位宣告為 volatile,可讓該欄位的讀寫受到 Java 記憶體模型所保證的可見性與順序性約束。一個執行緒寫入 volatile 欄位後,另一個執行緒再讀取同一欄位時,該次寫入 happens-before 該次讀取。因此,讀取端會看到該次寫入或之後對該欄位的寫入,而不是依賴過時的快取值。

這是一種明確定義的同步關係,並不代表每個執行緒都會在同一瞬間看到變更。如果某個執行緒之後不再讀取該欄位,就無法對變更作出反應;而單靠 volatile 存取,也不會同步其他無關的一般欄位。請讓執行緒間的通訊協定保持清楚:以同一個 volatile 欄位傳遞訊號;若其他共用狀態也必須與它協調,則使用另一種同步機制。

何時適合使用 volatile

當欄位的值可以獨立讀取並整體替換,而且寫入決策不依賴其他執行緒也可能變更的先前讀取結果時,適合使用 volatile 欄位。常見例子是布林值的關閉旗標:一個執行緒設定旗標,工作執行緒則持續檢查它。整體替換的設定值或狀態值也可能適用,前提是其相關不變條件不需要更大型的原子更新。

請先確認每次寫入是否都能獨立成立。如果執行緒必須先檢查共用狀態,再一致地更新一個或多個欄位,請使用鎖或合適的並行資料結構。單靠 volatile 無法確保這種多步驟協定安全。

volatile 無法保證的事項

volatile 的單次讀取或寫入各自具備可見性與順序性,但不會讓複合操作變成不可分割的單一操作。例如,count++ 會讀取 count、加一,再寫回結果。即使 count 是 volatile,兩個執行緒仍可能讀到相同的舊值,並互相覆蓋更新。若適用,可使用 AtomicInteger 進行原子數值更新;若該操作屬於更大的不變條件,則用 synchronized 或其他鎖保護整個操作。

同樣地,將多個欄位宣告為 volatile,也不會讓一組更新如同單一交易般發生。請依照程式需要完成的整體操作來選擇同步方式,而不只是看欄位是否共用。

使用 volatile 旗標停止執行緒

以下範例中的工作執行緒會檢查共用旗標,主執行緒則在短暫等待後清除旗標。Thread.start() 會建立獨立的工作執行緒;若直接呼叫 run(),方法會在呼叫端執行。此範例是一個完整獨立的 Java 類別。提供的成功執行紀錄顯示結束碼為 0;預期的標準輸出會列在程式後方。

執行此範例:

本節中的程式碼以及顯示的標籤或輸出仍為英文。

public class Main {
    private static volatile boolean running = true;

    public static void main(String[] args) throws Throwable {
        Thread worker = new Thread(() -> {
            while (running) {
                // Do a small unit of work here.
            }
            System.out.println("Worker stopped");
        });

        worker.start();
        Thread.sleep(50);
        running = false;
        worker.join();
    }
}
Worker stopped

工作執行緒只有在觀察到主執行緒寫入的 false 後才能結束。join() 會讓主執行緒等待工作執行緒結束,程式才會退出。在實際的工作執行緒中,請讓每個工作單位夠短,以便定期檢查旗標;如果執行緒可能長時間阻塞,也可能需要中斷或其他取消機制。這個迴圈只是精簡示範,等待期間會持續消耗 CPU。

比較 volatile 與 synchronized

volatile 適合用於簡單的可見性訊號,或可獨立替換的值。它不會取得監視器鎖、不會阻止多個執行緒同時執行臨界區,也不會讓複合更新具備原子性。synchronized 則透過進入及離開監視器建立可見性,並讓受同一監視器保護的程式碼以互斥方式執行。當多個步驟或欄位必須維持一致時,適合使用它。

兩者都沒有普遍適用的效能優勢。成本取決於競爭程度、存取模式、JVM,以及受保護的工作內容。請選擇保證符合需求的機制;若效能很重要,再測量實際應用程式。

重點整理

  • 若簡單的共用訊號或可獨立替換的欄位需要跨執行緒可見性,請使用 volatile。
  • 對該欄位的一次 volatile 寫入,以及之後對它的讀取,會形成 happens-before 關係。
  • 不要只靠 volatile 執行遞增、讀取後再寫入的邏輯,或維持跨多個欄位的不變條件。
  • 支援的單一值更新可使用原子類別;若必須協調完整操作,則使用 synchronized。