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

Java 多線程核心機制:從互斥到同步的進階實戰

     編輯:Java 相關知識

前言:為什麼需要更高級的線程協作?

在上一篇文章中,我們透過簡單的計數器示例理解了 synchronized 關鍵字如何實現「互斥」,確保同一時間只有一個線程能存取共享資源。然而,現實世界的併發場景遠比單純的「排隊進入」複雜。試想:如果線程 A 進入臨界區後發現條件不滿足(如緩衝區為空),它必須等待;此時線程 B 想進入生產資料,卻發現鎖被 A 持有而無法進入。這就是互斥機制的侷限性——等待中的線程依然佔用著鎖,導致其他線程無法進入甚至檢查條件。

經典案例:火車廁所模型

想像火車上只有一間廁所(共享資源/臨界資源),乘客(線程)想使用時:

  • 互斥模式:乘客 A 進去鎖門,乘客 B、C 在門外干等,門鎖始終掌握在 A 手中直到他出來。
  • 同步模式:乘客 A 進去發現沒手紙,他掛上「缺手紙」牌子並去休息區睡覺(wait()),釋放鎖。乘客 B 進來發現缺手紙,也去睡覺。乘客 C 是清潔員,進來補上手紙後敲醒休息區的人(notify()),大家再按順序使用。

同步機制的核心優勢在於:線程在條件不滿足時主動釋放鎖並進入等待狀態,不佔用 CPU 時間片,由其他線程在條件滿足時喚醒它。

代碼實戰:生產者-消費者模式(生活費帳戶)

我們用一個「學生取錢、家長存錢」的帳戶系統來完整演示同步機制:

核心類:帳戶

public class Account {
    private int balance = 0; // 共享資源

    // 學生取錢
    public synchronized void withdraw() {
        System.out.println("準備取錢!");
        try {
            if (balance == 0) {
                wait(); // 餘額為0,釋放鎖並進入等待集
            }
            System.out.println("剩餘:" + balance);
            balance -= 50; // 取走50元
            notify(); // 喚醒一個等待線程(可能是存錢的家長)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 家長存錢
    public synchronized void deposit() {
        System.out.println("準備存錢!");
        try {
            if (balance != 0) {
                wait(); // 還有錢,釋放鎖並等待
            }
            balance = 200; // 存入200元
            System.out.println("存入:" + balance);
            notify(); // 喚醒取錢的學生
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

線程角色

// 學生線程:每2秒嘗試取錢
public class StudentThread extends Thread {
    private Account account;
    public StudentThread(Account account) { this.account = account; start(); }
    public void run() {
        try {
            while (true) {
                Thread.sleep(2000);
                account.withdraw();
            }
        } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

// 家長線程:每12秒存錢
public class ParentThread extends Thread {
    private Account account;
    public ParentThread(Account account) { this.account = account; start(); }
    public void run() {
        try {
            while (true) {
                Thread.sleep(12000);
                account.deposit();
            }
        } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

測試啟動

public class TestAccount {
    public static void main(String[] args) {
        Account account = new Account(); // 唯一共享帳戶
        new StudentThread(account);
        new ParentThread(account);
    }
}

執行流程深度解析

  1. 程式啟動,兩個線程同時 start(),控制台輸出「準備取錢!」「準備存錢!」
  2. 學生線程先搶到鎖,進入 withdraw(),發現 balance == 0,呼叫 wait() 釋放鎖並進入等待集(睡覺)。
  3. 家長線程獲得鎖,進入 deposit(),發現 balance == 0 滿足存錢條件,存入 200,呼叫 notify() 喚醒等待集中的一個線程(此時只有學生)。
  4. 學生線程被喚醒,重新競爭鎖,獲得後繼續執行扣款 50,餘額 150,呼叫 notify()(此時無線程等待)。
  5. 學生線程每 2 秒循環取款,直到第 4 次取完餘額歸零,再次 wait() 睡眠。
  6. 家長線程 12 秒後醒來,發現餘額為零,存入 200,喚醒學生,循環往複。

關鍵點總結:wait() / notify() 協議

方法所屬類別前提條件行為
wait()Object必須在 synchronized 區塊中釋放當前物件鎖,當前線程進入該物件的等待集
notify()Object必須在 synchronized 區塊中隨機喚醒該物件等待集中的一個線程,喚醒後需重新競爭鎖
notifyAll()Object必須在 synchronized 區塊中喚醒該物件等待集中的所有線程

互斥 vs 同步:一個生動的比喻

互斥:老總正在用電話 A 談話,電話 B 響了。老總對 B 說:「我在講電話,你等一下」(不掛斷,佔用線路)。

同步:老總對 B 說:「我在講電話,等會我打回給你」(掛斷 B,釋放資源,稍後主動聯繫)。

同步機制讓「等待」變得主動且高效,是解決複雜併發協作問題的基石。

最佳實踐建議

  • 永遠在 while 迴圈而非 if 中檢查條件並呼叫 wait(),防止虛假喚醒:
    while (balance == 0) wait();
  • 優先使用 notifyAll() 避免信號遺漏,除非確定只有一類線程在等待。
  • 現代開發建議使用 java.util.concurrent 套件(ReentrantLock、Condition、BlockingQueue 等),語義更清晰、功能更強大。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved