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

以全域固定順序獲取多重鎖避免死鎖:Java 多執行緒程式設計實戰指南

     編輯:Java 程式設計入門

死鎖的成因與風險

在多執行緒環境中,當兩個或多個執行緒互相等待對方釋放資源時,便會發生死鎖。典型情境如下:執行緒 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 個唯一值,理論上極難耗盡,但在極長期運行系統中仍需評估。
  • 效能與記憶體代價:每個陣列需額外包裝物件,增加記憶體佔用與運行期開銷;若系統無法承受,應回頭審視鎖定架構設計。

最佳實務建議

  1. 優先採用 java.util.concurrent 套件(如 ReentrantLock、StampedLock)替代 synchronized,靈活度更高。
  2. 若必須手動管理多重鎖,永遠依固定全域順序獲取(如物件記憶體位址、唯一 ID、建立時間等)。
  3. 縮小同步區塊範圍,降低持有鎖時間,減少爭用機率。
  4. 善用靜態分析工具(SpotBugs、SonarQube)偵測潛在死鎖模式。

死鎖防範的核心在於設計階段就建立明確的鎖定層級,而非事後修補。掌握固定順序鎖定原理,並配合現代並發工具,才能寫出健壯的多執行緒系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved