1. Thread Dump and analysis of thread states
Thread Dump (thread dump) is a snapshot of the state of all threads in the application at a specific moment in time. It’s like a group photo of all your threads: who is doing what, who is stuck where, who is waiting for whom. Thread Dump — your primary tool for finding deadlocks, livelocks, and other mysterious hangs.
How to get a Thread Dump?
Via terminal (jstack):
If you have the Java process PID, run:
jstack <PID>
The command will print to the console the state of all threads, indicating which state each is in and which monitors (locks) it holds.
Via the IDE (IntelliJ IDEA):
In the “Run” menu → “Show Running List” → select the process → “Thread Dump”.
Via VisualVM or JConsole:
Open the process, find the “Threads” tab, and take a snapshot.
Thread Dump example
Dump fragment:
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
Here you can see that the “Thread-1” thread is blocked (BLOCKED), holds one monitor, but is waiting for another. If you see several such threads where one holds resource A and waits for B, and another holds B and waits for A — that’s a classic deadlock.
Thread states
| Status | Description |
|---|---|
| RUNNABLE | The thread is running or ready to run |
| BLOCKED | Waiting to acquire a monitor (lock) |
| WAITING | Waiting for notify()/notifyAll() (e.g., triggered by wait()) |
| TIMED_WAITING | Waiting with a timeout (e.g., sleep, wait(timeout)) |
| TERMINATED | Thread has finished |
Important: the RUNNABLE status does not always mean that the thread is executing right now — it is only ready to execute (the JVM scheduler may not run it immediately).
How to tell you have a deadlock?
In the dump, several threads are in the BLOCKED state, each waiting on a monitor held by another thread from the same set.
At the end of the dump, jstack usually prints:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
If threads remain in BLOCKED or WAITING for a long time, that’s a reason to investigate.
2. Monitoring and profiling threads
VisualVM
VisualVM — a free utility included with most JDKs. Lets you attach to a process, view thread states, take a Thread Dump, see CPU load, active and “hung” threads.
Threads tab: shows how many threads are created, their states, and the activity history.
Thread Dump: the “Thread Dump” button takes a snapshot similar to jstack.
Java Mission Control and Flight Recorder
Java Mission Control (JMC): an advanced tool for analyzing the JVM in real time. Helps explore locks, execution time, allocations, and latencies.
Java Flight Recorder (JFR): a built-in JVM profiler that collects events about threads, locks, pauses, etc.
Example: monitoring locks
In VisualVM or JMC you might see that:
- Thread “A” is blocked on object X.
- Thread “B” holds object X but is waiting for object Y.
- Thread “C” holds object Y but is waiting for object X.
This is a classic circular deadlock.
How to use these tools in practice?
- Run the application with -XX:+FlightRecorder (or just use JDK 11+).
- Open JMC, connect to the process, start a recording.
- Analyze hotspots, long-held locks, and contention between threads.
3. Logging and tracing
In multithreaded programs, debugging “by eye” is painful. Log entry/exit from critical sections (synchronized blocks), operations on shared variables, thread waits and wakeups — this way you’ll understand who acquired or released a resource and when.
How to log?
- Use standard tools: java.util.logging, SLF4J, Log4j.
- Log the thread name: Thread.currentThread().getName().
- Log timestamps and thread IDs.
- Log lock acquire/release events.
Logging example
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " acquired lock");
// critical section
System.out.println(Thread.currentThread().getName() + " is leaving lock");
}
Using thread names
Give threads meaningful names!
Thread t = new Thread(runnable, "MyWorker-1");
Tracing example using a logger
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " started work");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " entered synchronized");
// ...
}
logger.info(Thread.currentThread().getName() + " finished work");
}
}
4. Diagnostics best practices
Minimize lock scope
Hold locks for as little time as possible.
Bad example:
synchronized(lock) {
// long I/O
// heavy computations
// database access
// ... and only then work with shared data
}
Good example:
// outside synchronized: long I/O, computations
synchronized(lock) {
// only work with shared data
}
Use thread names
Meaningful thread names save time when analyzing dumps and logs.
Write multithreading tests
Use JUnit + CountDownLatch to simulate concurrency scenarios.
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // wait for both threads to finish
Use try-finally for ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
This way you won’t forget to release the lock even on exceptions. To avoid deadlocks, use tryLock() with a timeout.
Document why synchronization is needed
Comments like “synchronized is needed here because…” will help you understand the intent later.
5. Practice: analyzing a deadlock in a test program
Example code with a deadlock
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: acquired lockB");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: acquired lockB");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: acquired lockA");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
How to catch a deadlock
- Run the program — it will hang.
- Capture a thread dump (jstack or via VisualVM).
- Find “Thread-1” and “Thread-2” — you’ll see that each holds one lock and is waiting for the other.
- At the end of the dump there will be a “Found one Java-level deadlock” section.
How to fix
- Always acquire locks in the same order.
- Use ReentrantLock with tryLock() and a timeout: if you fail to acquire all locks — release and try again.
6. Common mistakes when diagnosing multithreaded programs
Mistake #1: Not knowing how to read a thread dump. Beginners get scared by a dump: “What are these weird stack traces and statuses?” In fact, it’s enough to know the main states and look for BLOCKED/WAITING to simplify the analysis.
Mistake #2: Ignoring thread names. Without meaningful names, understanding a dump is like looking for a needle in a haystack. Don’t be lazy — set names!
Mistake #3: Overly large synchronized blocks. If you synchronize large chunks of code, threads will block each other more often — this is visible by frequent BLOCKED in the dump.
Mistake #4: Confusing RUNNABLE with an actually running thread. RUNNABLE does not always mean it’s running on the CPU. The JVM scheduler decides whom to run.
Mistake #5: Not using monitoring tools. Many don’t know about VisualVM, JMC, Flight Recorder and struggle with println. Use the tools — they make life much easier.
Mistake #6: No logging of critical operations. Without logs, it’s almost impossible to understand who acquired/released a lock and when.
Mistake #7: Trying to catch data races “by eye”. Races don’t always manifest immediately — use tests with CountDownLatch, provoke contention via Thread.yield(), and analyze the state of shared variables.
GO TO FULL VERSION