1. 互斥鎖(Mutex):是什麼以及如何運作
互斥鎖(英文「mutual exclusion」,意即「互斥」)是一種機制,讓同一時間只有一個執行緒能執行臨界區程式碼。若互斥鎖已被佔用(被其他執行緒取得),其餘執行緒會等待直到它被釋放。
在 Java 中,常以同步的目標物件充當互斥鎖:synchronized。自 Java 5 版起,出現了類別 ReentrantLock——更明確且更彈性的互斥鎖實作。
示意
想像一個只有一把鑰匙(互斥鎖)的房間。要進入就必須拿到鑰匙。若鑰匙不在(已被他人拿走),你只能在門外等待。一旦鑰匙被放回(互斥鎖被釋放),下一位就可以進入。
Java 中互斥鎖的語法
透過 synchronized(經典作法):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
此處整個方法 increment 受互斥鎖保護——同一時間只有一個執行緒可以執行它。
透過 ReentrantLock(更彈性):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 取得互斥鎖
try {
count++;
} finally {
lock.unlock(); // 務必釋放!
}
}
}
重要!務必在 finally 區塊中釋放互斥鎖,否則可能出現「永久阻塞」(deadlock)——程式會掛住。
什麼時候需要互斥鎖?
當一次只能有一個執行緒使用某個資源時,就需要互斥鎖。該資源可以是變數、檔案或資料庫。當對資源的操作不是原子性時,使用互斥鎖尤為重要:即便是簡單的 count++,其實也包含三個步驟——讀取值、遞增並寫回。沒有互斥鎖,多個執行緒可能在步驟之間相互干擾,造成資料競爭。
2. 信號量(Semaphore):用途與運作方式
信號量是一種「調節器」,允許同時有多個執行緒使用資源,但不超過設定的數量。若達到上限,其餘執行緒會排隊等待。
類比:可停 3 輛車的停車場。車位滿了,後來的車必須等到有人離開。
Java 中信號量的語法
可使用類別 Semaphore,位於套件 java.util.concurrent:
import java.util.concurrent.Semaphore;
public class ParkingLot {
private final Semaphore spots;
public ParkingLot(int places) {
this.spots = new Semaphore(places);
}
public void parkCar(String car) throws InterruptedException {
spots.acquire(); // 嘗試佔用車位(若沒有則等待)
try {
System.out.println(car + " 已停好.");
Thread.sleep(1000); // 車子停在停車場
} finally {
spots.release(); // 釋放車位
System.out.println(car + " 已離開.");
}
}
}
用法:
ParkingLot parking = new ParkingLot(3);
for (int i = 1; i <= 5; i++) {
final String car = "車輛 " + i;
new Thread(() -> {
try {
parking.parkCar(car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
結果:同一時間停車場不會超過三輛車——其餘的會等待。
信號量如何運作?
- 建立信號量時需指定「許可」(permits)的數量。
- 方法 acquire() 嘗試取得許可:若有可用,執行緒通過;若沒有,則等待。
- 方法 release() 將許可歸還。
- 只有一個許可的信號量行為幾乎等同互斥鎖,但沒有「擁有者」。
3. 互斥鎖與信號量:有何差異?
| 特性 | 互斥鎖(Mutex) | 信號量(Semaphore) |
|---|---|---|
| 允許的執行緒數 | 僅一個 | 多個(受限數量) |
| 用途 | 保護資源 | 限制存取(例如池) |
| API в Java | |
|
| 管理方式 | 通常由「擁有者」 | 可由任一執行緒釋放 |
| 典型場景 | 共享計數器、物件 | 連線池、停車場、限流 |
- 互斥鎖——用於需要獨占存取的情況。
- 信號量——允許同時通過數個,但非全部。
類比:互斥鎖——只有一間隔間的洗手間;信號量——有三間隔間的洗手間。
4. 實務範例
範例 1:用互斥鎖保護臨界區
假設有一家共享的銀行,數個執行緒在帳戶間轉帳。這些操作必須是原子性的。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccount(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
public void withdraw(int amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock();
}
}
public int getBalance() {
return balance;
}
}
這裡所有對餘額的操作都由互斥鎖保護,以避免 race condition。
範例 2:用信號量限制存取
伺服器同時最多只能處理 2 個用戶端(例如受授權限制)。
import java.util.concurrent.Semaphore;
public class Server {
private final Semaphore connections = new Semaphore(2);
public void handleRequest(String client) throws InterruptedException {
connections.acquire();
try {
System.out.println(client + " 已連上伺服器.");
Thread.sleep(2000); // 模擬處理請求
} finally {
connections.release();
System.out.println(client + " 已斷線.");
}
}
}
用法:
Server server = new Server();
for (int i = 1; i <= 5; i++) {
final String client = "客戶端 " + i;
new Thread(() -> {
try {
server.handleRequest(client);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
結果:伺服器同時處理的用戶端不會超過兩個。
5. 使用重點與注意事項
互斥鎖:一定要釋放!
非常重要的是,即使拋出例外,也不能忘記呼叫 unlock()(或離開同步區塊)。請使用 try-finally:
lock.lock();
try {
// 臨界區
} finally {
lock.unlock();
}
若忘記,可能造成「永久阻塞」;其他執行緒將無限期等待。
信號量:可以替別人釋放嗎?
與互斥鎖不同,release() 可由任一執行緒呼叫,即使它沒有執行 acquire()。有時很方便,但也容易出錯——請遵守規範。
只有一個許可的 Semaphore = 互斥鎖?
幾乎是。但信號量沒有「擁有者」的概念:任何釋放都會增加許可計數。互斥鎖則必須由取得者自行釋放。
別把信號量當作池
信號量不是物件池,而只是「許可計數器」。它常被用來實作各種池(例如資料庫連線池),但本身不儲存任何物件。
6. 使用互斥鎖與信號量時的常見錯誤
錯誤 №1: 忘了呼叫 unlock/release。 如果你取得了互斥鎖或信號量,卻沒有呼叫 unlock() 或 release(),其他執行緒可能會永遠卡住。務必使用 try-finally,確保即使在發生例外時也能釋放鎖。
錯誤 №2: 在錯誤的物件上進行同步。 如果在非所有執行緒共享的變數上進行同步(例如區域變數或字串常值),同步將不起作用。
錯誤 №3: 重複釋放. 對於信號量而言:若呼叫 release() 的次數多於 acquire(),許可數會超出上限。請確保配對一致!
錯誤 №4: 用信號量取代互斥鎖(或反之)。 若需要獨占存取,請使用互斥鎖(synchronized 或 Lock)。若要限制同時運作的執行緒數量——請使用 Semaphore。
錯誤 №5: 鎖持有時間過長。 執行緒持有互斥鎖或信號量的時間越長,其它執行緒等待的時間就越久。請盡量縮短臨界區內的工作時間。
GO TO FULL VERSION