skip to content

How does jstack help you detect and diagnose a deadlock?

level: middleimportance: should knowfreq 57%

answer

  1. jstack auto-detects: 'Found one Java-level deadlock'
  2. Shows each thread, lock it holds, lock it waits for → the cycle
  3. Involved threads are BLOCKED; stacks printed
  4. Use jstack -l for ReentrantLock / ownable synchronizers
  5. Can't detect livelocks or condition/latch hangs (no lock cycle)

basics

~20 s

jstack automatically finds deadlocks. When two or more threads each hold a lock the other needs, jstack prints a 'Found one Java-level deadlock' section naming the threads and which lock each holds and waits for, so you see the cycle directly.

solid answer

~50 s

A deadlock is when two or more threads each hold a lock the other one needs, so none can proceed. jstack detects this for you: at the end of the dump it prints a `Found one Java-level deadlock:` section that lists each thread, the lock it currently holds, and the lock it is `waiting to lock` — forming a cycle (Thread-1 holds A waiting for B; Thread-2 holds B waiting for A). It even prints the stack traces of the involved threads so you can see the exact code lines acquiring the locks. The involved threads show state BLOCKED. This works for intrinsic `synchronized` monitors and, on modern JDKs, for `java.util.concurrent` ownable synchronizers like `ReentrantLock` (use `jstack -l` for full lock detail). Once you see the cycle, the fix is usually consistent lock ordering, lock timeouts (`tryLock`), or reducing lock scope. Note jstack detects classic lock-ordering deadlocks; it cannot detect livelocks or logical waits where no lock cycle exists.

code

java · 19 lines
java
// Two threads taking locks in opposite order -> deadlock
Object a = new Object(), b = new Object();

Thread t1 = new Thread(() -> {
    synchronized (a) {                 // holds A
        sleep(50);
        synchronized (b) { /* ... */ } // waits for B (held by t2)
    }
});
Thread t2 = new Thread(() -> {
    synchronized (b) {                 // holds B
        sleep(50);
        synchronized (a) { /* ... */ } // waits for A (held by t1)
    }
});
// jstack <pid> then prints:
//   Found one Java-level deadlock:
//   "Thread-0": waiting to lock <b>, held by "Thread-1"
//   "Thread-1": waiting to lock <a>, held by "Thread-0"

go deeper

for a junior

Knows jstack can report a deadlock and prints a 'Found one Java-level deadlock' message.

for a middle

Explains the lock-ordering cycle, reads which thread holds/waits for which lock, uses -l, and knows the BLOCKED state.

for a senior

Distinguishes lock-cycle deadlocks from livelocks/condition hangs jstack can't detect, and maps findings to ordering/tryLock fixes.

for a principal

Designs lock-ordering conventions and timeout policies to prevent deadlocks, and standardizes dump-based deadlock triage in runbooks.

## What a deadlock is A **deadlock** is a permanent standoff. Imagine two threads and two locks, A and B: 1. Thread-1 acquires lock **A**, then tries to acquire **B**. 2. Thread-2 acquires lock **B**, then tries to acquire **A**. Each now holds the lock the other is waiting for. Neither will ever release, so both wait forever. The application (or part of it) hangs. This is the classic **lock-ordering deadlock** caused by two code paths grabbing the same locks in opposite orders. ## How jstack surfaces it When you run `jstack <pid>`, the JVM **actively runs a deadlock detector** as it produces the dump. If it finds a cycle of threads each waiting on a lock held by the next, it appends a dedicated section: ``` Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x... (object 0x...a, a java.lang.Object), which is held by "Thread-2" "Thread-2": waiting to lock monitor 0x... (object 0x...b, a java.lang.Object), which is held by "Thread-1" ``` It then prints the **stack traces** of exactly those threads, so you can read the source lines where each lock was taken. Each involved thread's state is **BLOCKED**. This is the single most valuable feature of jstack for hang triage — you don't have to reconstruct the cycle by hand; the JVM hands it to you. ## Locks it understands - **Intrinsic monitors** — locks from the `synchronized` keyword. Always detected. - **Ownable synchronizers** — the `java.util.concurrent.locks` family (`ReentrantLock`, `ReentrantReadWriteLock`). Modern HotSpot detects deadlocks involving these too. Run **`jstack -l`** to print the full list of locked synchronizers each thread owns, which is essential when `ReentrantLock` is involved. ## What it cannot detect jstack finds **lock-cycle deadlocks** only. It will **not** flag: - a **livelock** (threads actively change state in response to each other but make no progress — no lock is held), - a **logical hang** where a thread waits on a condition/latch/external resource that never arrives (no lock cycle exists; you see a stuck WAITING thread but no 'deadlock' banner), - distributed deadlocks across processes or across a DB. For those, you fall back to reading the states and stacks manually (and taking several dumps over time to confirm the thread never moves). ## Fixing what jstack reveals Once the cycle is visible, the remedies are standard: - **Global lock ordering** — always acquire A before B everywhere, eliminating the opposite-order path. - **Lock timeouts** — `ReentrantLock.tryLock(timeout)` so a thread backs off instead of waiting forever. - **Reduce lock scope** / use a single coarser lock, or lock-free structures, to remove the need to hold two locks at once. ## Workflow `jps` to get the pid → `jstack -l <pid>` → scroll to the bottom for the `Found one Java-level deadlock` banner → read the named threads' stacks → fix the lock-ordering bug.

  • Why might jstack miss a hang even though the app is frozen?
    Because the hang isn't a lock cycle — e.g. a livelock, or a thread waiting on a latch/condition/external resource that never signals. No deadlock banner appears; you must read states and stacks yourself.
  • What flag improves deadlock detail for ReentrantLock?
    jstack -l, which also prints ownable synchronizers (java.util.concurrent locks), not just intrinsic monitors.
  • Once jstack shows the deadlock cycle, what's the most common fix?
    Impose a consistent global lock-acquisition order so no two paths grab the locks in opposite order; alternatively use tryLock with a timeout.

saying these in an interview costs you the question

  • Believing jstack detects every kind of hang — it only finds lock-ordering cycles, not livelocks or condition waits
  • Forgetting -l, then missing deadlocks involving ReentrantLock / java.util.concurrent locks
  • Thinking you must trace the cycle by hand — jstack prints the 'Found one Java-level deadlock' summary for you
  • Assuming a deadlocked thread is RUNNABLE; it is BLOCKED (waiting on a monitor)

context