A JVM thread dump is the closest you’ll get to a snapshot of your application’s soul, revealing not just what threads are doing, but why they’re stuck.
Let’s see a thread dump in action. Imagine a simple web application with two threads: WebServerThread handling incoming requests and DatabaseWorkerThread processing those requests.
// Simulating a critical section protected by a lock
public class Resource {
private final Object lock = new Object();
private String data;
public void update(String newData) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired lock.");
this.data = newData;
try {
Thread.sleep(1000); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " releasing lock.");
}
}
public String getData() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired lock.");
// Simulate work
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " releasing lock.");
return data;
}
}
}
public class DeadlockExample {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();
Thread thread1 = new Thread(() -> {
resource1.update("data1 from thread1");
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
resource2.update("data2 from thread1");
}, "Thread-1");
Thread thread2 = new Thread(() -> {
resource2.update("data2 from thread2");
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
resource1.update("data1 from thread2");
}, "Thread-2");
thread1.start();
thread2.start();
}
}
When you run this DeadlockExample, you’ll see output like this:
Thread-1 acquired lock.
Thread-2 acquired lock.
Thread-1 releasing lock.
Thread-2 releasing lock.
Thread-1 acquired lock.
Thread-2 acquired lock.
And then… nothing. The application hangs. This is where a thread dump becomes your detective tool.
A thread dump, often called a stack trace dump, captures the state of all threads in the JVM at a specific moment. It shows each thread’s name, ID, state (e.g., RUNNABLE, BLOCKED, WAITING), and the call stack leading to that state. This allows you to visualize the flow of execution and, crucially, identify circular dependencies where threads are waiting for resources held by each other.
To generate a thread dump for a running JVM process, you can use jstack. First, find the process ID (PID) of your Java application.
jps -l
This will list running Java processes and their main class names. Let’s say your application’s PID is 12345. Then, generate the dump:
jstack 12345 > threaddump.txt
Now, open threaddump.txt. You’ll see sections for each thread, looking something like this:
"Thread-1" #12 prio=5 tid=0x00007f8c4c001000 nid=0x12345 runnable [0x0000700001b13000]
java.lang.Thread.State: BLOCKED (on object monitor)
at Resource.update(DeadlockExample.java:16)
- waiting to lock <0x0000000780051000> (a java.lang.Object)
at DeadlockExample.lambda$main$0(DeadlockExample.java:31)
- locked <0x0000000780051010> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-2" #13 prio=5 tid=0x00007f8c4c002800 nid=0x54321 runnable [0x0000700001c15000]
java.lang.Thread.State: BLOCKED (on object monitor)
at Resource.update(DeadlockExample.java:16)
- waiting to lock <0x0000000780051010> (a java.lang.Object)
at DeadlockExample.lambda$main$1(DeadlockExample.java:38)
- locked <0x0000000780051000> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
The key indicators are BLOCKED (on object monitor) and the - waiting to lock <object_address> lines. In this dump:
Thread-1isBLOCKEDwaiting to lock theResourceobject at address0x0000000780051000.Thread-2isBLOCKEDwaiting to lock theResourceobject at address0x0000000780051010.
Crucially, look at the - locked <object_address> lines:
Thread-1holds the lock on the object at0x0000000780051010.Thread-2holds the lock on the object at0x0000000780051000.
This forms the classic deadlock: Thread-1 needs what Thread-2 has, and Thread-2 needs what Thread-1 has.
The most common cause of deadlocks is incorrect lock ordering. If all threads always acquire locks in the same global order, deadlocks are prevented. For example, if both Thread-1 and Thread-2 always tried to acquire resource1’s lock before resource2’s lock, the deadlock would be avoided.
Another common pattern is livelock, where threads are active but repeatedly change their state in response to each other without making progress. This often looks similar to deadlock in a thread dump, with threads in a WAITING or TIMED_WAITING state, potentially in a loop of trying to acquire locks or communicate.
Contention, where multiple threads are competing for the same limited resource (like a lock, database connection, or CPU time), is often revealed by threads stuck in BLOCKED or RUNNABLE states for extended periods. High CPU usage from RUNNABLE threads often points to busy-waiting or inefficient algorithms.
To fix the deadlock in our example, you’d modify the thread logic to ensure consistent lock acquisition order. For instance, always locking resource1 then resource2:
// ... in DeadlockExample.java ...
Thread thread1 = new Thread(() -> {
synchronized (resource1) { // Acquire resource1 first
System.out.println("Thread-1 acquired resource1 lock.");
resource1.update("data1 from thread1");
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (resource2) { // Acquire resource2 second
System.out.println("Thread-1 acquired resource2 lock.");
resource2.update("data2 from thread1");
}
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
synchronized (resource1) { // Acquire resource1 first
System.out.println("Thread-2 acquired resource1 lock.");
resource2.update("data2 from thread2"); // Note: This call is now inside the resource1 lock
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (resource2) { // Acquire resource2 second
System.out.println("Thread-2 acquired resource2 lock.");
resource1.update("data1 from thread2"); // Note: This call is now inside the resource2 lock
}
}
}, "Thread-2");
// ... rest of the code
This fix ensures that if Thread-1 holds resource1’s lock, Thread-2 will wait for it before attempting to acquire resource1’s lock, breaking the circular dependency.
Beyond explicit deadlocks, you’ll often see threads in WAITING or TIMED_WAITING states due to Object.wait(), Thread.sleep(), or Lock.lockInterruptibly() without a corresponding notify() or notifyAll(). This is a common cause of perceived hangs.
Another subtle issue can arise with volatile variables. While they ensure visibility, they don’t provide atomicity for compound operations. If a thread is reading a volatile variable, performing a calculation, and then writing back, another thread could interleave and corrupt the operation, leading to incorrect states that might appear as hangs if subsequent operations depend on that state.
Finally, remember that the JVM itself uses threads. If the JVM’s internal threads (like garbage collection threads) are heavily burdened or stuck, it can manifest as a general slowdown or unresponsiveness in your application threads, which might appear as contention or blocking in your thread dumps.
After fixing the deadlock, the next issue you’ll likely encounter is a StackOverflowError if your recursive methods don’t have a proper base case.