CodeGym /課程 /JAVA 25 SELF /Deadlock:成因、範例、排除

Deadlock:成因、範例、排除

JAVA 25 SELF
等級 53 , 課堂 0
開放

1. 深入探討 Deadlock

Deadlock(「互相封鎖」)是指兩個或多個執行緒彼此無限期等待:每個執行緒都持有某個資源,並嘗試取得另一個已被其他執行緒佔用的資源。結果誰都無法繼續工作,程式就「卡住」了。就像兩輛車在狹窄的橋上迎面相遇:若沒有一方退後,誰也過不去。

在 Java 中,deadlock 不是例外Exception),而是程式的「卡死」。執行緒不會結束或丟出錯誤,而是彼此無限期等待。因此這類問題很陰險:往往只在「特定情況」下才會浮現。

Deadlock 發生的四個條件

  • 1. 互斥(Mutual Exclusion)
    某資源在同一時間只能被一個執行緒持有(例如物件監視器、檔案)。
  • 2. 保持並等待(Hold and Wait)
    執行緒持有一個資源,同時在不釋放既有資源的情況下再去請求另一個資源。
  • 3. 不可搶占(No Preemption)
    外部無法「奪走」資源:只能由持有該資源的執行緒自行釋放。
  • 4. 循環等待(Circular Wait)
    存在一條閉合的等待鏈,每個執行緒都在等待下一個執行緒所持有的資源。

只有同時滿足以上所有條件,才可能發生 deadlock。只要打破其中任一條件,互相封鎖就不會發生。

2. Deadlock 的程式碼範例

示意

  • 有兩個資源:lock1lock2(一般物件)。
  • 執行緒 A 先取得 lock1,再嘗試取得 lock2
  • 執行緒 B 先取得 lock2,再嘗試取得 lock1

範例(請勿在家嘗試!)

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // 執行緒 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("執行緒 1: 已取得 lock1");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("執行緒 1: 嘗試取得 lock2");
                synchronized (lock2) {
                    System.out.println("執行緒 1: 已取得 lock2");
                }
            }
        });

        // 執行緒 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("執行緒 2: 已取得 lock2");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("執行緒 2: 嘗試取得 lock1");
                synchronized (lock1) {
                    System.out.println("執行緒 2: 已取得 lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

執行時會發生什麼?

  • 執行緒 1 先取得 lock1,執行緒 2 先取得 lock2
  • 兩者都在 100 毫秒內暫停,足以先拿到第一把鎖。
  • 接著各自嘗試取得第二把鎖,但那把鎖已被對方持有。
  • 兩個執行緒無限期地彼此等待——出現 deadlock。

主控台會看到:

執行緒 1: 已取得 lock1
執行緒 2: 已取得 lock2
執行緒 1: 嘗試取得 lock2
執行緒 2: 嘗試取得 lock1

為什麼會發生 deadlock?詳細解析

  • 互斥:每把鎖同一時間只能被一個執行緒持有。
  • 保持並等待:執行緒 1 持有 lock1 並等待 lock2;執行緒 2 持有 lock2 並等待 lock1
  • 不可搶占:外部無法把鎖「拔走」——只能離開 synchronized 區塊後才會釋放。
  • 循環等待:兩個執行緒之間形成了循環等待。

3. 如何避免與預防 deadlock

以相同順序取得資源

首要規則:當多個執行緒需要取得多個資源時——務必在所有位置都用相同的順序。例:所有地方都先 lock1,再 lock2

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // 統一順序 "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // 同時操作兩個資源
        }
    }
}

在統一順序下不可能形成循環等待——deadlock 的條件被破壞。

使用帶逾時的 tryLock(ReentrantLock

當事先無法確定需要哪些資源時,使用 ReentrantLocktryLock 方法,以避免無限期等待。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // 臨界區
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("無法取得 lock2,回退");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("無法取得 lock1,回退");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

優點:可以回退操作並稍後再試。缺點:程式碼較複雜,但可以避免 deadlock。

最小化持有鎖的時間

盡可能縮短持鎖時間。在 synchronized/lock 中只做必要的事;其餘邏輯移到臨界區之外。

避免巢狀鎖

synchronized/lock 的巢狀層級越少,發生互相封鎖的風險越低。如果可行——針對相關資源使用單一把鎖。

使用現成的 thread-safe 結構

標準程式庫已解決許多問題:例如 ConcurrentHashMap 等集合與 java.util.concurrent 中的其他類別,可降低 deadlock 風險並簡化程式碼。

4. deadlock 的診斷

Thread Dump 與 jstack

Thread Dump 是 JVM 中所有執行緒狀態的快照。

在主控台取得:

jstack <pid>

其中 <pid> 是 Java 行程的識別碼(可透過 jps 取得)。

在 IDE 的除錯面板中通常有「Thread Dump」按鈕。

在 dump 中請留意:

  • 狀態 BLOCKEDWAITING
  • 類似 waiting to lock ...locked ... 的訊息。
  • JVM 的訊息:"Found one Java-level deadlock:"——當偵測到互相封鎖時會出現。

Dump 片段範例:

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

這就是 deadlock:執行緒彼此「互相持有」。

VisualVM 與其他工具

  • VisualVM——免費的執行緒分析與 deadlock 偵測工具。
  • Java Mission Control / Flight Recorder——進階的監控與剖析。

在 VisualVM 中可檢視執行緒樹、其狀態,並獲得互相封鎖的通知。

表格:「如何避免陷入 deadlock」

deadlock 的原因 如何避免
以不同順序取得資源 一律遵守統一的資源取得順序
巢狀的 synchronized 最小化巢狀層級;可行時使用單一把鎖
持鎖時間過長 縮短臨界區內的工作
同時使用多把鎖 搭配逾時的 tryLock 並具備回退邏輯
非 thread-safe 的集合 使用並行集合(如 ConcurrentHashMap 等)

5. 處理 deadlock 的常見錯誤

錯誤 №1:以不同順序取得資源。最常見的原因是不同執行緒以不同順序取得鎖。即使「看似更快」,也務必遵守統一順序——不然就難免循環等待。

錯誤 №2:不必要的巢狀 synchronized多餘的巢狀會增加互相封鎖風險並使程式碼更複雜。請簡化鎖模型。

錯誤 №3:忽略 tryLock 與逾時。使用 synchronized 可能讓執行緒無限期等待。若無把握,請使用帶逾時的 tryLock 並實作回退邏輯。

錯誤 №4:在持鎖期間執行耗時工作。網路請求、I/O 或重度計算都會在持鎖時大幅提高風險。請將其移出臨界區。

錯誤 №5:不分析 Thread Dump。Thread Dump 是追查互相封鎖的最佳利器。請使用 jstack,分析 BLOCKED/WAITING 狀態與「holding/waiting to lock」的鏈結。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION