Java Thread Dumps: Find Deadlocks, Starvation, and Lock Contention
Use repeated thread dumps and thread-state patterns to distinguish deadlocks from slow dependencies, starvation, and excessive synchronization.
On this page
Key takeaways
- One thread dump is a snapshot; repeated dumps reveal whether threads make progress.
- A deadlock has a cycle of threads waiting on locks held by each other.
- BLOCKED, WAITING, and RUNNABLE states need interpretation alongside stack frames and lock ownership.
- Fix lock ordering, critical-section size, or resource bounds; do not restart blindly before preserving evidence.
Capture evidence while the incident is present
When a Java service stops making progress, preserve several thread dumps a few seconds apart along with request latency, CPU, GC, and dependency metrics. jcmd <pid> Thread.print -l is a useful JVM diagnostic command; jstack is another option depending on runtime and deployment. Confirm the process and container namespace before targeting a PID, and protect dumps because stack traces can contain sensitive values.
A single snapshot cannot distinguish a temporarily busy thread from one permanently stuck. Compare the same thread IDs and stacks across dumps. If the frames and lock ownership never change, the thread may be blocked or waiting on a stalled dependency. A thread in RUNNABLE can still be waiting in native I/O, so state names are clues, not complete diagnoses.
Recognize a deadlock cycle
A classic deadlock has thread A holding lock 1 while waiting for lock 2, and thread B holding lock 2 while waiting for lock 1. JVM dumps often report a detected deadlock and include ownable synchronizers. The general fix is to acquire multiple locks in a consistent global order, reduce nested lock acquisition, or redesign ownership so operations do not need both locks simultaneously.
Deadlock is different from starvation, where a thread repeatedly fails to obtain a resource, and from lock contention, where progress exists but throughput is limited by serialized work. Compare blocked stacks, lock owners, CPU consumption, and operation rates to tell them apart.
Contention and thread-pool exhaustion
If many request threads are blocked on the same monitor or connection pool, inspect the owner and what it does while holding the resource. Slow logging, network calls, callbacks, or large computations inside a synchronized region extend lock duration. Shrink the critical section and move independent work outside it while preserving invariants.
A saturated executor can look like an application deadlock: all workers may be waiting for tasks queued to the same exhausted pool, or tasks may synchronously wait for child work that cannot start. Check active count, queue depth, rejection count, and dependency wait times. Use bounded queues and explicit rejection/backpressure rather than unbounded thread creation.
Turn the dump into a testable fix
Map each stuck operation to its lock, pool, queue, and external dependency. Add a regression test for lock order or executor behavior, then load test for progress and tail latency. A restart may restore service, but capture diagnostics first when safe; otherwise the evidence needed to prevent recurrence disappears.