skip to content

How do you diagnose a hung Java application you suspect is deadlocked, using a thread dump?

level: middleimportance: must knowfreq 58%

answer

  1. jstack / jcmd Thread.print / kill -3 (SIGQUIT)
  2. Look for BLOCKED threads + 'Found one Java-level deadlock'
  3. Match 'waiting to lock <id>' to who 'locked <id>'
  4. Take 2–3 dumps to confirm it's stuck, not transient
  5. ThreadMXBean.findDeadlockedThreads() for programmatic checks

basics

~20 s

Take a thread dump (jstack <pid>, jcmd, or kill -3) and look for threads in BLOCKED state. The JVM usually prints a 'Found one Java-level deadlock' section naming the threads and the locks they each hold and wait for.

solid answer

~50 s

When the app hangs, capture a thread dump with jstack <pid>, jcmd <pid> Thread.print, kill -3 <pid> (sends SIGQUIT, dump goes to stdout), or via JMX/VisualVM/JConsole. The JVM's built-in deadlock detector scans the lock graph and, if it finds a cycle, prints a clear 'Found one Java-level deadlock:' block listing each involved thread, the lock it's waiting to lock, and which thread holds it — plus the stack traces. Even without that block, you read it manually: look for threads in BLOCKED state, note the monitor each is 'waiting to lock' and the one it already holds ('locked'), and trace whether those form a cycle. Tools like VisualVM, JMC, or ThreadMXBean.findDeadlockedThreads() automate this. Note the built-in detector finds monitor and ReentrantLock cycles but not deadlocks via Semaphores, CountDownLatch, or external resources like DB locks — those you must reason about from the stacks.

code

java · 14 lines
java
// Programmatic watchdog: detect deadlocks at runtime
import java.lang.management.*;

ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
long[] ids = tmx.findDeadlockedThreads(); // monitors + ownable synchronizers
if (ids != null) {
    ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);
    for (ThreadInfo ti : infos) {
        System.out.println(ti.getThreadName()
            + " waiting on " + ti.getLockName()
            + " held by " + ti.getLockOwnerName());
    }
}
// CLI equivalents: jcmd <pid> Thread.print | jstack <pid> | kill -3 <pid>

go deeper

for a junior

Knows how to take a thread dump (jstack/jcmd) and that BLOCKED threads and a 'deadlock' section indicate the problem.

for a middle

Reads the dump: matches 'waiting to lock' object ids to holders, takes multiple dumps, uses the auto-detector.

for a senior

Knows the detector's limits (non-monitor/external locks) and uses ThreadMXBean / pool analysis for those cases.

for a principal

Builds production observability: watchdog using findDeadlockedThreads, alerting, and runbooks; reasons about pool-exhaustion and distributed deadlocks the JVM can't see.

## When to suspect deadlock The app **hangs**: requests stop completing, CPU may be near-idle (blocked threads don't burn CPU), but the process is alive. That idle-but-stuck profile is the tell — a busy loop would peg a core; a deadlock parks threads. ## Capturing a thread dump A **thread dump** is a snapshot of every JVM thread and its stack at one instant. Ways to get one: - `jstack <pid>` — prints all stacks to stdout (JDK tool). - `jcmd <pid> Thread.print` — the modern, preferred command. - `kill -3 <pid>` (Unix) / Ctrl-Break (Windows console) — sends **SIGQUIT**; the JVM dumps to its **stdout** (often the app log), not to the terminal you ran kill from. - GUI: **VisualVM**, **JConsole**, **JDK Mission Control** — all can dump and even highlight deadlocks. Capture **two or three** dumps a few seconds apart: if the same threads are stuck at the same lines every time, it's a real block, not a momentary wait. ## Reading the dump Each thread entry shows a **state** and a stack. Relevant states: - `BLOCKED` — waiting to acquire a monitor someone else holds (the deadlock signature). - `WAITING`/`TIMED_WAITING` — in `wait()`, `park()`, `sleep()`, `join()` — usually *not* a monitor deadlock. Within a stack you'll see lines like: ``` - waiting to lock <0x...> (a com.x.Account) // wants this monitor - locked <0x...> (a com.x.Account) // already holds this one ``` Match the **object id** a thread is 'waiting to lock' to the thread that has it 'locked'. If thread T1 waits for a monitor T2 holds, and T2 waits for one T1 holds, that's the cycle. ## The JVM's automatic detector The HotSpot JVM scans for **monitor and `java.util.concurrent` Lock** cycles when you take a dump and, if it finds one, appends an explicit section: ``` Found one Java-level deadlock: ============================= "Thread-A": waiting to lock monitor ... which is held by "Thread-B" "Thread-B": waiting to lock monitor ... which is held by "Thread-A" ``` This names the threads, the locks, and the holders directly — usually all you need. ## Programmatic detection `ThreadMXBean` (via `ManagementFactory.getThreadMXBean()`) exposes `findDeadlockedThreads()` (monitors + ownable synchronizers) and `findMonitorDeadlockedThreads()`. You can poll it from a watchdog and log/alert on deadlock in production. ## What the detector misses The built-in detector only knows about intrinsic monitors and AbstractOwnableSynchronizer-based locks (ReentrantLock, ReentrantReadWriteLock). It does **not** detect deadlocks formed via `Semaphore` permits, `CountDownLatch`/`CyclicBarrier`, `wait/notify` mis-coordination, thread-pool exhaustion (e.g. a pool task waiting on another task in the same bounded pool), or **external** locks (database row locks, distributed locks). For those you read the stacks and reason about the wait graph yourself, or use DB-side tools. ## Putting it together 1. Confirm a hang (idle CPU, requests not completing). 2. Take 2–3 dumps via `jcmd Thread.print`. 3. Look for the 'Found one Java-level deadlock' block; if present, read the named threads/locks. 4. If absent but you still suspect it, find BLOCKED threads and trace 'waiting to lock' vs 'locked' object ids into a cycle, or consider a non-monitor/external cause.

  • Why might the JVM's automatic deadlock section be empty even though the app is clearly hung on a deadlock?
    The built-in detector only finds cycles over intrinsic monitors and AbstractOwnableSynchronizer locks (ReentrantLock). Deadlocks via Semaphore permits, latches/barriers, thread-pool exhaustion, wait/notify, or external DB/distributed locks aren't reported — you must trace those from the stacks or DB tooling.
  • Why capture multiple thread dumps a few seconds apart?
    A single dump can catch a thread in a momentary, legitimate wait. If several dumps show the same threads blocked at the same lines on the same locks, it confirms a true, persistent deadlock rather than transient contention.

saying these in an interview costs you the question

  • Expecting kill -3 output in the terminal — it goes to the JVM's stdout/log
  • Assuming the auto-detector catches Semaphore/latch/DB/pool deadlocks (it doesn't)
  • Treating WAITING/TIMED_WAITING threads as deadlock — those are usually normal waits
  • Taking a single dump and over-concluding from one snapshot

context