StackTraces
Java已驗證

Java 死鎖排查 jstack 報 Found one Java-level deadlock

@debug_master32 天前更新閱讀 6 min00

一句話先說結論:死鎖不是例外,是幾個執行緒各自握著對方要的鎖、互相永久等待。用 jstack <pid>,JVM 會直接打出 Found one Java-level deadlock: 段,標出環上每個執行緒。本質是四個必要條件同時成立,打破任意一個即可,最常用兩招——鎖順序一致、tryLock 加超時。

背景

synchronized 保證同一時刻只有一個執行緒進入臨界區。多把鎖嵌套使用時,如果兩個執行緒按相反順序拿鎖,就會互相卡死,誰也沒法繼續。

教科書式的現場是兩個物件 lockA、lockB:

public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                sleep(100);
                synchronized (lockB) { /* ... */ }
            }
        }, "Thread-1").start();

        new Thread(() -> {
            synchronized (lockB) {
                sleep(100);
                synchronized (lockA) { /* ... */ }
            }
        }, "Thread-2").start();
    }
}

Thread-1 先拿 lockA 再拿 lockB,Thread-2 反過來,正好構成一個環。

現象

死鎖最陰險的地方:程式不報錯、不拋例外、不打印日誌,就那麼在原地僵住。控制台最後幾行通常是:

Thread 1: acquired lockA
Thread 2: acquired lockB

然後就沒有然後了。

有經驗的老手會先 jps 拿到進程號,再 jstack <pid> 抓執行緒快照。死鎖時 jstack 末尾會直接給出結論,用一段輸出把環畫清楚:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001e0c7800 (object 0x000000001e0c7800, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e0c8800 (object 0x000000001e0c8800, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at DeadlockDemo.lambda$main$0(DeadlockDemo.java:12)
        - waiting to lock <0x000000001e0c7800> (a java.lang.Object)
        - locked <0x000000001e0c8800> (a java.lang.Object)
        at DeadlockDemo$$Lambda$1/0x0000000800b20840.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:833)
"Thread-2":
        at DeadlockDemo.lambda$main$1(DeadlockDemo.java:20)
        - waiting to lock <0x000000001e0c8800> (a java.lang.Object)
        - locked <0x000000001e0c7800> (a java.lang.Object)
        at DeadlockDemo$$Lambda$2/0x0000000800b20840.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:833)

這段輸出就是定位死鎖的金標準:誰在等誰、鎖被誰佔,一目瞭然。jcmd <pid> Thread.print 也能拿到同樣的信息。

根因分析

死鎖能成立,四個條件必須同時滿足:

  1. 互斥:同一把鎖同一時刻只能一個執行緒持有。
  2. 持有並等待:執行緒握著一把鎖,還想拿下一把。
  3. 不可搶佔:沒法從外面把鎖搶過來,只能執行緒自己釋放。
  4. 循環等待:執行緒之間形成「A 等 B、B 等 A」的環。

前面例子裡,Thread-1 握著 lockA 等 lockB,Thread-2 握著 lockB 等 lockA,四個條件全中。根子是加鎖順序不一致。

死鎖不一定非得是顯式的 synchronized。JDK-8209007 報告過一個更隱蔽的:外層並行流裡調用 ConcurrentHashMap.compute,compute 內部又嵌套了 Stream.parallel,兩個工作執行緒在兩個 ReservationNode 上互相等待,同樣打出 Found one Java-level deadlock。這說明框架內部的自旋和遞歸併行,一樣會製造循環等待。

解決方案

首選:加鎖順序一致

給所有需要同時拿多把鎖的路徑定死一個順序,全局遵守。先 lockA 後 lockB,任何地方都不顛倒:

public void transfer() {
    synchronized (lockA) {
        synchronized (lockB) {
            // 固定顺序,环就断了
        }
    }
}

順序一致後,「循環等待」被破壞,死鎖從根上不可能發生。代價是鎖多了之後,維護順序的成本變高。

tryLock 加超時:拿不到就退

當加鎖順序不好統一,改用 ReentrantLock.tryLock,拿不到就超時放棄,別無限等:

ReentrantLock a = new ReentrantLock();
ReentrantLock b = new ReentrantLock();

public boolean tryWork() throws InterruptedException {
    if (a.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (b.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // 干活
                    return true;
                } finally {
                    b.unlock();
                }
            }
        } finally {
            a.unlock();
        }
    }
    return false; // 拿不到就放弃或重试
}

思路是破壞「持有並等待」:要麼一次拿齊,要麼暫時全放掉。比裸 synchronized 多了退路,但程式碼更繁瑣,還可能引入活鎖。

縮小臨界區、少嵌套

能不同時拿兩把鎖就別拿。把真正需要加鎖的部分縮到最小,程式碼裡少出現「鎖套鎖」。

思考過程

jstack 那段 Found one Java-level deadlock 不是每個死鎖都一定打印。更復雜的鎖(如 ReentrantReadWriteLock、CountDownLatch、Semaphore)卡住時,執行緒狀態是 WAITING 而非 BLOCKED,JVM 未必一眼認出是死鎖。這時要靠多次抓執行緒快照,觀察執行緒是否長期停在同一個 WAITING 點。

JDK-8209007 最終被標記為 Not an Issue,不是因為沒有死鎖,而是官方認為「在 ConcurrentHashMap.compute 裡跑嵌套並行流」超出了預期用法,屬於使用方式問題。這個案例說明,排查死鎖不能只盯著自己寫的鎖,框架內部的鎖同樣會中招。

定位方法論就一句話:先 jstack 拿完整執行緒快照,別隻盯 BLOCKED,長期不動的 WAITING 往往就是環上的另一頭。

小結

  • 死鎖的特徵是「假死、無例外、無日誌」,光看現象很難第一時間想到。
  • 一抓一個準的工具是 jstack <pid>,認準 Found one Java-level deadlock 那段。
  • 打破四個必要條件之一能根治,最實用的是鎖順序一致加 tryLock 超時。
  • 別隻懷疑 synchronized,框架內部和併發集合同樣會死鎖。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法