在多執行緒環境中,當兩個或多個執行緒互相等待對方釋放資源時,便會發生死鎖。典型情境如下:執行緒 A 持有資源 1 並等待資源 2,而執行緒 B 持有資源 2 並等待資源 1,雙方均不願先釋放手中資源,導致程式永久停擺。死鎖往往隱藏在看似正常的程式碼中,且難以在測試階段重現,是多執行緒開發中最棘手的問題之一。
考慮以下計算兩個陣列元素總和的方法:
public int sumArrays(int[] a1, int[] a2) {
int value = 0;
int size = a1.length;
if (size == a2.length) {
synchronized(a1) { // 先鎖定 a1
synchronized(a2) { // 再鎖定 a2
for (int i = 0; i < size; i++)
value += a1[i] + a2[i];
}
}
}
return value;
}
此程式碼雖正確鎖定兩個陣列,但若兩個執行緒以相反順序傳入相同陣列(如執行緒 1 呼叫 sumArrays(ArrayA, ArrayB),執行緒 2 呼叫 sumArrays(ArrayB, ArrayA)),便會因鎖定順序不一致而導致死鎖。
最簡單的做法是規定所有呼叫端必須以相同順序傳入參數。然而,這極度依賴開發者自律,專案規模一大極易失控,屬於「事後諸葛亮」式的被動防禦。
更穩健的作法是讓被鎖定物件自帶「鎖定順序」屬性,並在獲取鎖前依此排序。以下以包裝類別 ArrayWithLockOrder 示範:
class ArrayWithLockOrder {
private static long numLocks = 0;
private final long lockOrder;
private final int[] arr;
public ArrayWithLockOrder(int[] a) {
arr = a;
synchronized (ArrayWithLockOrder.class) {
numLocks++;
lockOrder = numLocks; // 每個實例獲得唯一遞增序號
}
}
public long lockOrder() { return lockOrder; }
public int[] array() { return arr; }
}
class SomeClass implements Runnable {
public int sumArrays(ArrayWithLockOrder a1, ArrayWithLockOrder a2) {
int value = 0;
ArrayWithLockOrder first = a1, last = a2;
int size = a1.array().length;
if (size == a2.array().length) {
// 依 lockOrder 決定鎖定順序
if (a1.lockOrder() > a2.lockOrder()) {
first = a2; last = a1;
}
synchronized (first) {
synchronized (last) {
int[] arr1 = first.array();
int[] arr2 = last.array();
for (int i = 0; i < size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() { /* ... */ }
}
numLocks 為 static 變數,多執行緒同時建立實例時必須用 synchronized(ArrayWithLockOrder.class) 保護,確保序號唯一。long 可表示 9.22×1018 個唯一值,理論上極難耗盡,但在極長期運行系統中仍需評估。ReentrantLock、StampedLock)替代 synchronized,靈活度更高。死鎖防範的核心在於設計階段就建立明確的鎖定層級,而非事後修補。掌握固定順序鎖定原理,並配合現代並發工具,才能寫出健壯的多執行緒系統。