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

Java 多執行緒同步實戰:synchronized 關鍵字與銀行帳戶競態條件解決方案

     編輯:Java 相關知識

多執行緒同步核心問題:競態條件

當多執行緒同時存取共享可變資料時,若缺乏同步機制,會發生競態條件,導致資料不一致。本文以銀行帳戶存提款為例,示範問題重現與 synchronized 解決方案。

1. 競態條件重現:未同步版本

class Account {
    private String name;
    private float balance;

    public Account(String name, float balance) {
        this.name = name;
        this.balance = balance;
    }

    // 存款:非同步
    public void deposit(float amount) {
        float tmp = balance;
        tmp += amount;
        sleep(100); // 模擬資料庫操作延遲
        balance = tmp;
    }

    // 提款:非同步
    public void withdraw(float amount) {
        float tmp = balance;
        tmp -= amount;
        sleep(100);
        balance = tmp;
    }

    public float getBalance() { return balance; }
    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

// 測試:1000 執行緒各存 100 再提 100,預期餘額 1000
public class AccountTest {
    private static final int THREADS = 1000;
    public static void main(String[] args) throws InterruptedException {
        Account acc = new Account("John", 1000f);
        Thread[] threads = new Thread[THREADS];

        for (int i = 0; i < THREADS; i++) {
            threads[i] = new Thread(() -> {
                acc.deposit(100f);
                acc.withdraw(100f);
            });
            threads[i].start();
        }

        for (Thread t : threads) t.join();
        System.out.println("最終餘額: " + acc.getBalance()); // 非 1000!
    }
}

執行結果(每次不同)

最終餘額: 3900.0
最終餘額: 4900.0
最終餘額: 4700.0
最終餘額: 5200.0

2. 問題分析

  • 非原子性操作:balance = tmp 非單一指令,中間可被切換執行緒。
  • 共享可變狀態:balance 為競爭資源。
  • 交錯執行:執行緒 A 讀取 balance,執行緒 B 也讀取同值,各自計算後寫回,導致一方更新遺失。

3. synchronized 解決方案

方法層級同步

public synchronized void deposit(float amount) {
    float tmp = balance;
    tmp += amount;
    sleep(1); // 縮短睡眠仍會觸發競態
    balance = tmp;
}

public synchronized void withdraw(float amount) {
    float tmp = balance;
    tmp -= amount;
    sleep(1);
    balance = tmp;
}

機制:每個物件擁有內建鎖(monitor)。呼叫 synchronized 方法需取得該物件鎖,釋放後其他執行緒才能進入。

區塊同步(細粒度控制)

public void deposit(float amount) {
    synchronized (this) { // 或 synchronized(lockObject)
        float tmp = balance;
        tmp += amount;
        sleep(1);
        balance = tmp;
    }
}

4. 執行結果驗證

加上 synchronized 後,多次執行皆輸出:

最終餘額: 1000.0

5. 為何 getBalance() 不需同步?

  • 單純讀取 float(32 位元)在 JVM 為原子操作(JLS 17.7)。
  • 但若為 long/double(64 位元),32 位元 JVM 可能非原子,需 volatile 或同步。
  • 最佳實務:讀寫皆同步或用 AtomicReference/AtomicLong,確保可見性與一致性。

6. 進階同步工具(JDK 5+)

工具適用場景
ReentrantLock需可中斷、超時、公平鎖、條件變數
ReadWriteLock讀多寫少,提升並發度
AtomicInteger/Long/Reference單一變數無鎖原子操作
ConcurrentHashMap高並發 Map
CountDownLatch/CyclicBarrier執行緒協調等待
Semaphore資源池許可控制

7. 最佳實務總結

  1. 識別共享可變狀態 → 最小化共享範圍。
  2. 優先用不可變物件、執行緒區域變數、無鎖資料結構。
  3. 必須同步時:synchronized 簡單場景,Lock 複雜需求。
  4. 鎖粒度:越小越好,但避免過度細分增加複雜度。
  5. 避免在同步區塊呼叫外部方法/耗時操作。
  6. 死鎖預防:固定鎖獲取順序、使用 tryLock(timeout)。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved