# Reading a Thread Dump Without Guessing

> A thread dump answers precise questions if you ask them precisely. What each state really means, and the traps that produce confident wrong diagnoses.

- Published: 2026-08-19
- Tags: concurrency, profiling, java, virtual-threads
- Source: https://jvmscope.com/blog/reading-a-thread-dump-without-guessing/
- Language: en-US
- Author: Marcus Venn

---
A thread dump is the least fashionable diagnostic tool available for a JVM and still one of the most decisive. It costs a safepoint, needs no agent, works on any JDK, and answers the question incidents actually turn on: what is every thread in this process waiting for right now?

The reason it has a reputation for being unhelpful is that it is usually read wrong.

## Take it properly

Use `jcmd`. `jstack` still works but is the older interface, and `jcmd` runs the same operation through the standard diagnostic command channel:

```
for i in 1 2 3 4 5; do
  jcmd $(pgrep -f my-app.jar) Thread.print > dump-$i.txt
  sleep 5
done
```

Five dumps, five seconds apart. This matters more than anything else in this article. A single dump tells you where threads were at one instant, which is nearly useless for distinguishing a thread parked in its normal idle state from one that has been stuck for four minutes. Five dumps tell you which stacks did not move — and a stack that is byte-identical across thirty seconds is the finding.

If the process is unresponsive enough that `jcmd` cannot attach, `kill -3` sends `SIGQUIT` and the JVM writes a dump to its standard output. That path costs nothing and requires no working attach mechanism.

## What the states mean, precisely

Each thread's header line carries a state, and two of the five are routinely misread.

**RUNNABLE** means the thread is not blocked on a Java-level monitor or parked. It does **not** mean the thread is consuming CPU. A thread blocked in a socket read sits in `RUNNABLE` for the entire duration, because from the JVM's point of view it is executing a native method and the kernel wait is invisible. This single fact accounts for most "the application is CPU-bound" diagnoses that turn out to be network waits. If you need to know which threads are actually burning CPU, take `top -H -p <pid>`, convert the native thread ids to hexadecimal, and match them against the `nid=` field in the dump — on Linux, where that field is hexadecimal. The dump also carries a per-thread `cpu=` figure, which answers the same question without leaving the file.

**BLOCKED** means the thread is waiting to enter a `synchronized` block that another thread holds. This is the unambiguous one, and it comes with the evidence attached:

```
"http-nio-8080-exec-42" #187 [19003] daemon prio=5 os_prio=0 cpu=41.02ms elapsed=612.44s
        tid=0x00007f8a08c21000 nid=0x4a3b waiting for monitor entry  [0x00007f89f4bfe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.Registry.lookup(Registry.java:88)
        - waiting to lock <0x00000006c2a41f38> (a com.example.Registry)
        at com.example.Handler.handle(Handler.java:41)
```

The address in `waiting to lock` is the key. Search the same dump for `- locked <0x00000006c2a41f38>` and you have the thread that is holding it, and its stack tells you what it is doing instead of releasing it.

**WAITING** and **TIMED_WAITING** mean the thread called `Object.wait()`, `LockSupport.park()`, `Thread.sleep()` or an equivalent. A thread pool worker with no work is `WAITING` on its queue, which is the healthy idle state and not a finding. The trap here is the opposite of the RUNNABLE trap: dozens of `WAITING` threads look alarming and usually mean the system is quiet.

## The three shapes worth recognising

**Deadlock.** The JVM detects monitor cycles itself and appends a section titled `Found one Java-level deadlock` with the participating threads and the locks each holds and wants. If it is there, the diagnosis is complete; go read the two stacks. Note that this detector covers monitors and `Lock` implementations, but not a lock ordering problem expressed through semaphores or database row locks — those you will have to see yourself.

**Pool exhaustion.** Every thread in a pool sharing the same frame, several levels down, in `BLOCKED` or in a native call. The pool is not the problem; whatever they are all waiting on is. Count them: if `http-nio-8080-exec-*` appears two hundred times and the pool maximum is two hundred, requests are queuing outside the dump entirely.

**Convoy behind one slow resource.** Many threads `RUNNABLE` inside a driver's socket read, all against the same endpoint. This looks like load and is actually one dependency responding slowly, with the thread pool acting as an accidental queue.

## Naming is a decision you make in advance

The single highest-leverage change you can make to your future thread dumps is naming threads properly today. `pool-3-thread-17` tells an incident responder nothing. `payment-reconcile-17` tells them which subsystem is stuck before they read a single frame.

```java
ThreadFactory factory = Thread.ofPlatform()
        .name("payment-reconcile-", 0)
        .daemon(true)
        .factory();
```

If you use a framework's executor abstraction, set the prefix there. It costs one line and it is the difference between reading a dump and decoding one.

## Virtual threads change the format

A process running a million virtual threads cannot produce a useful plain-text dump, and `Thread.print` deliberately does not include them. JDK 21 added a format built for this:

```
jcmd <pid> Thread.dump_to_file -format=json threads.json
```

The JSON output groups virtual threads by the structured concurrency scope that created them, which turns "a million stacks" into a tree that mirrors the shape of the work. It also shows which carrier each mounted virtual thread is running on — useful when the question is whether carriers are saturated or whether something is pinned.

For the platform threads that still matter — carriers, the common pool, framework internals — `Thread.print` remains the right tool, and the reading habits above are unchanged.

## The discipline

Take several dumps. Compare them before interpreting any one of them. Trust `BLOCKED` and its lock addresses. Distrust `RUNNABLE` until you have checked it against per-thread CPU. Look for what is identical across dumps, not for what looks alarming in one.

Done that way, a thread dump is not a wall of text. It is the runtime answering a direct question about every thread it has, and it answers faster than most of the tooling built to avoid reading it.
