在上一篇文章中,我們透過簡單的計數器示例理解了 synchronized 關鍵字如何實現「互斥」,確保同一時間只有一個線程能存取共享資源。然而,現實世界的併發場景遠比單純的「排隊進入」複雜。試想:如果線程 A 進入臨界區後發現條件不滿足(如緩衝區為空),它必須等待;此時線程 B 想進入生產資料,卻發現鎖被 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);
}
}
start(),控制台輸出「準備取錢!」「準備存錢!」withdraw(),發現 balance == 0,呼叫 wait() 釋放鎖並進入等待集(睡覺)。deposit(),發現 balance == 0 滿足存錢條件,存入 200,呼叫 notify() 喚醒等待集中的一個線程(此時只有學生)。notify()(此時無線程等待)。wait() 睡眠。wait() / notify() 協議| 方法 | 所屬類別 | 前提條件 | 行為 |
|---|---|---|---|
wait() | Object | 必須在 synchronized 區塊中 | 釋放當前物件鎖,當前線程進入該物件的等待集 |
notify() | Object | 必須在 synchronized 區塊中 | 隨機喚醒該物件等待集中的一個線程,喚醒後需重新競爭鎖 |
notifyAll() | Object | 必須在 synchronized 區塊中 | 喚醒該物件等待集中的所有線程 |
互斥:老總正在用電話 A 談話,電話 B 響了。老總對 B 說:「我在講電話,你等一下」(不掛斷,佔用線路)。
同步:老總對 B 說:「我在講電話,等會我打回給你」(掛斷 B,釋放資源,稍後主動聯繫)。
同步機制讓「等待」變得主動且高效,是解決複雜併發協作問題的基石。
while 迴圈而非 if 中檢查條件並呼叫 wait(),防止虛假喚醒:while (balance == 0) wait();notifyAll() 避免信號遺漏,除非確定只有一類線程在等待。java.util.concurrent 套件(ReentrantLock、Condition、BlockingQueue 等),語義更清晰、功能更強大。