首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

深入理解 JVM (六):Java 記憶體模型、線程併發與 volatile 關鍵字詳解

     編輯:Java 綜合教學

1. Java 記憶體模型 (JMM)

CPU 執行速度遠快於主記憶體讀寫,引入高速快取 (L1/L2/L3 Cache) 緩解速度差距。JMM 抽象對應:

  • 主記憶體 — 物理記憶體,所有線程共享變量儲存區
  • 工作記憶體 — 每條線程私有,對應 CPU 高速快取,存放共享變量副本

JMM 架構圖

線程操作變量必須在工作記憶體進行,無法直接讀寫主記憶體。不同線程間變量傳遞需經主記憶體,這就是併發可見性問題的根源。

2. JMM 八大原子操作

規則:read/load、store/write 必須成對出現,保證操作有序性。

3. 併發編程三大特性

  1. 原子性:操作不可分割,要麼全執行要麼全不執行。JMM 透過 lock/unlock 保證,Java 對應 synchronized。
  2. 可見性:一線程修改共享變量,其他線程立即可見。volatile、synchronized、final 可保證。
  3. 有序性:單線程內有序,多線程觀察可能無序 (指令重排)。volatile 禁止指令重排,synchronized 鎖內有序。

指令重排範例

// 線程 1
int a = readFileName();
writeFile(a);
initialized = true;  // 可能被重排到 writeFile 之前!

// 線程 2
while (!initialized) { sleep(); }
doSomeThing();  // 若 initialized 先為 true,a 尚未初始化,邏輯錯誤

單線程語義不變,但多線程下順序關鍵。volatile 可修飾 initialized 禁止重排。

4. volatile 關鍵字雙重語義

  1. 可見性:寫入 volatile 變量立即刷新到主記憶體;讀取時強制從主記憶體獲取最新值。
  2. 禁止指令重排:編譯器/運行時不會重排 volatile 讀寫與其前後操作。

經典失效場景:非原子操作

public volatile int inc = 0;
public void increase() { inc++; }  // 非原子:讀取-修改-寫入 三步

// 200 線程各增 50 次,預期 10000,實際 < 10000
// 原因:inc++ 非原子,volatile 僅保證可見性,不保證原子性

結論:volatile 不保證原子性,複合操作需用 synchronized、AtomicInteger 或 Lock。

volatile 有序性邊界

volatile boolean initialized = false;
public void run() {
    context = readFileName();  // 1
    writeFile(context);        // 2 (1,2 可重排)
    initialized = true;        // 3 (volatile 寫)
    play();                    // 4
    drawable();                // 5 (4,5 可重排)
}

volatile 寫 (3) 之前的操作 (1,2) 不會重排到 (3) 之後;volatile 寫 (3) 之後的操作 (4,5) 不會重排到 (3) 之前。但 (1,2) 內部、(4,5) 內部仍可重排。這是「部份有序性」。

5. 實測代碼與現象

public class VolatileTest2 {
    public volatile int inc = 0;
    static CountDownLatch latch = new CountDownLatch(200);

    public void increase() { inc++; }

    public static void main() {
        VolatileTest2 test = new VolatileTest2();
        for (int i = 0; i < 200; i++) {
            new Thread(() -> {
                for (int j = 0; j < 50; j++) test.increase();
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println(test.inc);  // 輸出 9950 (非 10000)
    }
}

作者註:Android 23 (API 23) 測試竟恆為 10000,原因未明 (可能 JIT 優化或測試環境差異)。

6. 總結

  • JMM 定義線程與主記憶體交互規範,解決可見性、有序性、原子性
  • volatile 輕量級同步工具:保證可見性 + 禁止指令重排
  • volatile 不能 替代 synchronized 解決原子性問題
  • 適用場景:狀態標記 (boolean flag)、單次寫入多次讀取、雙重檢查鎖 (DCL) 模式

參考資源

  1. 上一篇文章:
  2. 下一篇文章:
Spider RPC 框架安全機制深度解析:四層防護體系 Hibernate 入門指南:環境搭建與首個 ORM 範例實作 JSP 開發筆記:session.getAttribute() 在 Java 代碼中的等價寫法 JS 載入時機陷阱:為什麼
操作作用對象說明
lock主記憶體變量標記變量為線程獨佔
unlock主記憶體變量釋放獨佔狀態
read主記憶體變量從主記憶體傳輸變量到工作記憶體
load工作記憶體變量將 read 取得的值放入工作記憶體副本
use工作記憶體變量傳遞變量值給執行引擎
assign工作記憶體變量執行引擎值賦給工作記憶體副本
store工作記憶體變量將工作記憶體值傳輸回主記憶體
write主記憶體變量將 store 取得的值寫入主記憶體變量