1. ReentrantLock 類:彈性的鎖定
關鍵字 synchronized 很適合基本情境:可以快速、簡單地保護方法或程式碼區塊。但有時我們需要更多:
- 顯式管理鎖(例如嘗試取得鎖,若失敗就不等待)。
- 將資源的「讀取」與「寫入」權限分離。
- 可中斷等待鎖。
- 診斷是誰、何時取得或釋放了鎖。
針對這些需求,ReentrantLock 與 ReadWriteLock 應運而生。它們比傳統的 synchronized 提供更多控制與功能。
它到底是什麼?
ReentrantLock 是實作介面 Lock 的類別。它的運作大致和 synchronized 類似,但有更多「配備」。最大的差異是鎖的控制變為顯式:你需要自行呼叫 lock() 與 unlock()。
有個重點——reentrant 的意思是「可重入」,同一個執行緒可以連續多次取得同一把鎖而不導致死鎖。當方法遞迴呼叫自身,或在呼叫鏈中共用同一把鎖時,這會很有用。
使用語法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int value = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 取得鎖
try {
value++;
} finally {
lock.unlock(); // 務必釋放鎖!
}
}
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
}
請注意:
呼叫 lock() 與 unlock() 時,一定要搭配 try...finally。若忘了呼叫 unlock(),其他執行緒將無法進入受保護的區塊——會造成永無止盡的阻塞。
ReentrantLock 的功能
嘗試取得:
可以嘗試取得鎖,但不要無限期等待:
if (lock.tryLock()) {
try {
// 執行工作
} finally {
lock.unlock();
}
} else {
// 未取得鎖—改做其他事
}
帶逾時的等待:
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
// 在 100 毫秒內取得鎖
}
檢查鎖是否已被持有:
if (lock.isLocked()) { ... }
等待佇列診斷、「公平性」鎖模式 等其他實用功能。
3. 範例:使用 ReentrantLock 遞增計數器
讓我們擴充主控台應用程式(例如模擬來自不同執行緒的訂單處理),比較使用 synchronized 與 ReentrantLock 的寫法。
使用 synchronized 的範例
public class OrderCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
使用 ReentrantLock 的等價範例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OrderCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
有什麼好處?
- 可嘗試取得鎖而不必無限期等待(tryLock())。
- 能實作更複雜的邏輯:例如以特定順序取得多把鎖(適用於複雜資料結構)。
- 可以在不同位置「解鎖」(但務必非常謹慎——一定要記得 unlock()!)。
4. ReadWriteLock:讀寫鎖
它是什麼?
ReadWriteLock 不只是鎖,更像是聰明的存取協調器。其主要實作是 ReentrantReadWriteLock,並將鎖分為兩類:讀鎖與寫鎖。
當執行緒只進行讀取且沒有人修改資料時,可以同時並行——讀取不會互相干擾。但一旦有人要寫入,其餘都必須等待:寫入只允許單一參與者,且需要獨占。
這在「讀多寫少」的情境特別有用——例如使用者經常瀏覽的商品目錄,但只偶爾更新。
使用語法
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ProductCatalog {
private final Map<String, String> products = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void addProduct(String id, String name) {
rwLock.writeLock().lock();
try {
products.put(id, name);
} finally {
rwLock.writeLock().unlock();
}
}
public String getProduct(String id) {
rwLock.readLock().lock();
try {
return products.get(id);
} finally {
rwLock.readLock().unlock();
}
}
}
在我們的應用中的使用範例
假設我們有一個訂單資料庫,所有執行緒會讀取(例如搜尋訂單),但偶爾會有新訂單加入(寫入操作)。
import java.util.*;
import java.util.concurrent.locks.*;
public class OrderDatabase {
private final List<String> orders = new ArrayList<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 新增訂單(需要 writeLock)
public void addOrder(String order) {
rwLock.writeLock().lock();
try {
orders.add(order);
} finally {
rwLock.writeLock().unlock();
}
}
// 取得所有訂單的副本(可並行讀取)
public List<String> getOrders() {
rwLock.readLock().lock();
try {
// 回傳副本以避免競態
return new ArrayList<>(orders);
} finally {
rwLock.readLock().unlock();
}
}
}
發生了什麼?
- 只要沒有寫入,成千上萬的執行緒都能同時讀取訂單清單。
- 一旦某個執行緒開始新增訂單——讀取就會被封鎖,以避免出現不一致的資料。
5. 比較:何時該用哪一種?
| 情境 | synchronized | ReentrantLock | ReadWriteLock |
|---|---|---|---|
| 簡單同步 | ✔ | ✔ | ✖(過度) |
| 需要逾時/嘗試取得 | ✖ | ✔ | ✔ |
| 讀多寫少 | ✖ | ✖ | ✔(顯著提升) |
| 需要診斷/統計 | ✖ | ✔ | ✔ |
| 遞迴鎖定 | ✔ | ✔(可重入性) | ✔ |
結論:
- 簡單情境——使用 synchronized。
- 需要彈性——ReentrantLock。
- 「常讀少寫」——ReadWriteLock。
6. 視覺化:ReadWriteLock 的運作示意
flowchart LR
subgraph 讀取
T1[執行緒 1] -- 讀取 --> Orders
T2[執行緒 2] -- 讀取 --> Orders
T3[執行緒 3] -- 讀取 --> Orders
end
subgraph 寫入
T4[執行緒 4] -- 寫入 (addOrder) --> Orders
end
Orders[訂單清單]
style Orders fill:#f9f,stroke:#333,stroke-width:2px
只要沒有任何執行緒在寫入,大家就能同時讀取。一旦出現寫入,其餘執行緒要等到 writeLock 完成。
7. 實作細節與注意事項
「公平性」(fairness)
在 ReentrantLock 與 ReentrantReadWriteLock 中,可以啟用「公平」模式(fair mode):執行緒依佇列順序服務,而非搶占式的「誰搶到就先用」。這能避免執行緒飢餓,但可能降低效能。
Lock fairLock = new ReentrantLock(true); // true — 公平模式
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);
潛在陷阱
- 遺漏 unlock: 若未呼叫 unlock(),將造成永久鎖定。務必使用 try...finally。
- 鎖內發生例外: 即使程式區塊內拋出例外,鎖仍必須被釋放!
- 過度使用 ReadWriteLock: 對於小型集合,或幾乎總是寫入的情況——ReadWriteLock 意義不大,反而讓程式碼更複雜。
8. 常見錯誤
錯誤 №1:忘了呼叫 unlock()
最常見、也最棘手的錯誤,是在取得鎖後忘記呼叫 unlock()。結果就是永久鎖定,執行緒「卡住」。即使覺得「這裡不會出事」,也要一律使用 try...finally。
錯誤 №2:在不需要的地方使用 ReadWriteLock
如果幾乎沒有平行讀取,而寫入很頻繁,ReadWriteLock 只會讓程式碼更複雜且降低效能。僅在確實存在大量同時讀取時才使用它。
錯誤 №3:以不同順序取得多把鎖
若你的程式碼會取得多把 Lock(例如對多個物件),請在所有執行緒中一律以相同的順序取得。否則可能發生 deadlock——執行緒會彼此無限等待。
錯誤 №4:把 synchronized 隨便換成 ReentrantLock
不要不加思索地把所有 synchronized 都換成 Lock——這不一定會加速程式,還可能讓程式碼更難閱讀。
錯誤 №5:忽略可重入性
同一個執行緒連續多次呼叫 lock() 對 ReentrantLock 來說是正常的,但別忘了 unlock() 需要呼叫相同次數!
GO TO FULL VERSION