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

Java 多執行緒資料同步深度解析:從 Dirty Read 到記憶體模型

     編輯:Java 相關知識

核心問題:共享變數的可見性與原子性

Java 變數分為區域變數(執行緒私有,無同步問題)與類變數/實例變數(堆上共享,需同步)。當多執行緒對共享變數進行「讀-改-寫」非原子操作時,會發生髒資料。

經典反例:100 執行緒計數器只得到 59

public class MyThread extends Thread {
    public static int n = 0; // 共享類變數

    public void run() {
        int m = n;   // 讀取
        yield();     // 主動讓出 CPU,擴大競態窗口
        m++;         // 修改區域副本
        n = m;       // 寫回
    }

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new MyThread());
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println("n = " + n); // 預期 100,實際常見 59~99
    }
}

為什麼會少算?時序圖解析

執行緒 A                    執行緒 B
  │                            │
  ├─ m = n (讀到 0)            │
  ├─ yield() 讓出 CPU          │
  │                            ├─ m = n (讀到 0)
  │                            ├─ yield()
  │                            ├─ m++ → 1
  │                            ├─ n = 1
  ├─ (恢復) m++ → 1            │
  ├─ n = 1  (覆蓋 B 的結果!)   │
  │                            │
結果:n=1,少算了 1 次

連 n++ 都不是原子操作!位元碼證明

Java 原始碼:          n++;
Javap 反編譯位元碼:
   0: getstatic     #2  // 讀取 n 值到操作數堆疊
   3: iconst_1              // 推入常數 1
   4: iadd                  // 整數相加
   5: putstatic     #2      // 寫回 n
   8: return
// 三步驟:讀取 → 計算 → 寫回,中間任何時刻被切換都會丟失更新

Java 記憶體模型(JMM)根本原因

  • 主記憶體:儲存所有共享變數的「金標準」值。
  • 工作記憶體:每個執行緒私有,包含共享變數的副本(快取、暫存器)。
  • 執行緒操作共享變數流程:主記憶體 → 複製到工作記憶體 → 運算 → 寫回主記憶體。
  • 無同步時,各執行緒工作記憶體副本可能不一致,寫回順序不可預期,導致更新丟失。

同步解決方案對比

方案關鍵字/類別原理適用場景效能
synchronized 方法synchronized鎖住 this / Class 物件,進入需取得監視器鎖簡單同步、粒度較粗中
synchronized 區塊synchronized(obj)鎖住指定物件,縮小臨界區高併發、精細控制中高
volatilevolatile強制從主記憶體讀寫,禁止指令重排,**不保證複合操作原子性**單一變數狀態旗標、雙重檢查鎖高
原子類別AtomicInteger 等CAS 無鎖演算法,硬體級原子指令計數器、序號生成、狀態更新高
Lock 介面ReentrantLock顯式鎖,支援公平/非公平、可中斷、超時、條件變數複雜同步邏輯、需嘗試獲取鎖中高

修正計數器範例:三種寫法

1. synchronized 靜態方法(鎖 Class 物件)

public static synchronized void inc() { n++; }
public void run() { for(int i=0;i<10;i++) inc(); }

2. AtomicInteger(推薦高併發計數)

public static AtomicInteger n = new AtomicInteger(0);
public void run() { for(int i=0;i<10;i++) n.incrementAndGet(); }

3. volatile + CAS 迴圈(教學用,生產用 AtomicInteger)

public static volatile int n = 0;
public void run() {
    for(int i=0;i<10;i++) {
        int cur, next;
        do { cur = n; next = cur + 1; }
        while (!UNSAFE.compareAndSwapInt(this, offset, cur, next)); // 需 Unsafe
    }
}

volatile 陷阱:複合操作不保證原子性

volatile int n = 0;
// n = n + 1;  // 非原子!讀取 n、加 1、寫回中間可被打斷
// n++;        // 同上
// 只有單一讀/寫(n = 5; int x = n;)才具原子性

最佳實踐清單

  1. 優先使用不可變物件:final、String、Guava Immutable 集合,根除共享狀態。
  2. 次優先用 java.util.concurrent:Atomic*、ConcurrentHashMap、CopyOnWriteArrayList、ThreadLocal。
  3. 再考慮 synchronized/Lock:保護複雜不變條件、多變數原子操作。
  4. 謹慎用 volatile:僅用於「單一變數、單一寫入者、多讀取者」的狀態旗標(如 volatile boolean shutdown)。
  5. 正確發布物件:final、volatile、安全發布模式(靜態初始化、volatile 參考、ThreadLocal)。

核心口訣:共享可變狀態是萬惡之源;能不共享就不共享,能不可變就不可變,必須共享可變時,選對同步工具,守住原子性與可見性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved