skip to content

How do you use /actuator/threaddump to diagnose a hang or deadlock — what fields matter and what do thread states tell you?

level: seniorimportance: should knowfreq 38%

answer

  1. ThreadDumpEndpoint = jstack over HTTP
  2. state: BLOCKED=monitor contention, WAITING=parked
  3. lockedMonitors/lockedSynchronizers + lockOwnerName
  4. deadlock = ownership cycle (JSON won't say 'deadlock')
  5. take 2-3 dumps 5s apart — stuck vs moving

basics

~20 s

The thread dump lists every thread with its state and stack trace. Many threads BLOCKED on the same lock, or two threads each holding a lock the other wants, points to contention or a deadlock. RUNNABLE threads stuck in the same frame suggest a hot loop.

solid answer

~40 s

/actuator/threaddump (ThreadDumpEndpoint) is a jstack over HTTP. Each entry has the thread's name, threadState (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, etc.), stackTrace, and lock info: lockedMonitors, lockedSynchronizers, and lockInfo/lockOwnerName for what it's waiting on. To diagnose: request text via Accept: text/plain, then scan for patterns. Lots of pool threads BLOCKED waiting on the same monitor means lock contention or exhaustion. Two threads each holding a lock the other wants — an A→B / B→A cycle in lockOwnerName — is a classic deadlock; a live jstack also prints an explicit 'deadlock detected' section. Many RUNNABLE threads stuck at the same stack frame indicates a CPU-hot loop or a slow downstream call. Threads WAITING/TIMED_WAITING in a connection-pool getConnection frame indicate pool starvation. Take two dumps a few seconds apart to see what's moving versus stuck.

code

java · 14 lines
java
// Programmatic deadlock check — actuator JSON won't synthesize this for you
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.lang.management.ThreadInfo;

ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
long[] deadlocked = tmx.findDeadlockedThreads(); // null if none
if (deadlocked != null) {
    for (ThreadInfo ti : tmx.getThreadInfo(deadlocked, true, true)) {
        System.out.println(ti.getThreadName()
            + " waiting on " + ti.getLockInfo()
            + " owned by " + ti.getLockOwnerName());
    }
}

go deeper

for a junior

Know it shows all threads with their states and stack traces.

for a middle

Distinguish RUNNABLE/BLOCKED/WAITING/TIMED_WAITING and read a stack trace.

for a senior

Correlate lock ownership to find contention/deadlock and use the multi-dump technique.

for a principal

Tie thread-dump patterns to systemic issues (pool sizing, downstream latency, back-pressure) and prefer it over heapdump for low-impact live triage.

**What it is.** `ThreadDumpEndpoint` returns, per live thread: `threadName`, `threadId`, `threadState` (a `java.lang.Thread.State`), `priority`, `daemon`, the `stackTrace` (array of frames with class/method/file/line), and lock details — `lockedMonitors` (intrinsic `synchronized` monitors held), `lockedSynchronizers` (AQS locks like `ReentrantLock`/`ReadWriteLock` held), `lockInfo` (the object the thread is currently blocked/waiting on), and `lockOwnerId`/`lockOwnerName` (which thread owns that lock). JSON by default; `Accept: text/plain` yields the familiar jstack layout. **Thread states and what they imply.** - `RUNNABLE` — executing or ready to. Many RUNNABLE threads parked at the *same* frame across two consecutive dumps = a hot CPU loop or a blocking native/IO call that the JVM still reports as runnable (e.g. socket read). - `BLOCKED` — waiting to acquire a `synchronized` monitor another thread holds. Many threads BLOCKED on one `lockInfo` identity hash = **lock contention**; the `lockOwnerName` tells you the culprit holding it. - `WAITING` — indefinitely waiting (`Object.wait()`, `LockSupport.park()`, `Thread.join()`, `Condition.await()`). Common and often benign (idle pool threads), but a request thread WAITING in `getConnection` = pool starvation. - `TIMED_WAITING` — same but with a timeout (`sleep`, `wait(ms)`, `poll(timeout)`). - `NEW` / `TERMINATED` — not started / finished. **Deadlock diagnosis.** A deadlock is a cycle: thread A holds lock L1 (BLOCKED trying to get L2), thread B holds L2 (BLOCKED trying to get L1). In the dump you trace it via `lockedMonitors`/`lockedSynchronizers` (what each holds) and `lockInfo` + `lockOwnerName` (what each waits for and who owns it) forming a cycle. Note: `jstack` on a live process prints an explicit **"Found one Java-level deadlock"** section; the raw actuator JSON does *not* synthesize that summary for you — you correlate the ownership yourself (or use `ThreadMXBean.findDeadlockedThreads()` programmatically). This is a real gotcha in interviews. **Method.** (1) Reproduce the symptom. (2) Grab the dump — ideally **two or three dumps ~5 seconds apart**. Threads whose stack *doesn't change* between dumps are truly stuck; threads that move are just busy. (3) Group by state and by the top few stack frames to spot the dominant pattern. (4) For contention, follow `lockOwnerName` to the holder and look at *its* stack — that's usually the root cause (e.g. one thread doing slow IO inside a `synchronized` block while everyone queues). **Common patterns.** - Tomcat `http-nio-*-exec` threads all BLOCKED / WAITING in a downstream client call → the backing service is slow and the request thread pool is saturating. - Threads WAITING in `HikariPool.getConnection` → DB pool exhausted (connections not returned / too small pool). - One thread RUNNABLE in a tight `while` loop across dumps → CPU spin bug. **Comparisons.** It's the HTTP-reachable equivalent of `jstack <pid>` or `kill -3` (which prints a dump to stdout). It's cheap compared to heapdump — a thread dump is a fast, low-impact snapshot with no full GC — so it's safe to take repeatedly in production. Combine with `/actuator/metrics` (thread counts, pool gauges) for the quantitative side.

  • Why take multiple thread dumps a few seconds apart instead of one?
    A single snapshot can't distinguish a thread that's genuinely stuck from one that's merely busy. Comparing dumps: threads whose stack is unchanged across snapshots are truly blocked/hung; threads whose frames change are making progress.
  • Does the actuator threaddump JSON explicitly tell you a deadlock exists?
    No. Unlike jstack's live output, the actuator JSON gives you per-thread lock ownership/wait info but does not synthesize a 'deadlock detected' summary — you correlate lockOwnerName cycles yourself, or use ThreadMXBean.findDeadlockedThreads().
  • You see many http-nio-exec threads in WAITING at HikariPool.getConnection — what does that mean?
    Database connection-pool starvation: request threads are blocked waiting for a connection because the pool is exhausted (too small, or connections held too long / leaked). Fix the leak or size the pool/timeouts appropriately.

saying these in an interview costs you the question

  • Thinking BLOCKED and WAITING mean the same thing
  • Assuming the actuator JSON prints an explicit deadlock section like jstack
  • Diagnosing from a single dump and calling a busy thread 'stuck'
  • Believing a thread dump is as heavy as a heap dump

context