Create your own
Lesson illustration

Thread Dump Analysis & Deadlock Prevention

Hello! Welcome back to our module on Advanced Concurrency and Performance.

In our last lesson, we explored how to manage race conditions using locks (synchronized, ReentrantLock) to protect critical sections of code. While locks are essential for ensuring data integrity, they introduce a serious risk of their own: deadlock. This is a classic, severe concurrency problem that can freeze parts of your application, or the entire system, and is a frequent topic in senior-level engineering interviews.

Today, we'll tackle this head-on. Our learning outcome is to analyze a thread dump to identify a deadlock and explain deadlock prevention strategies like lock ordering. We will learn how to diagnose a deadlock in a production-like scenario using a thread dump and, more importantly, how to design your code to prevent deadlocks from ever happening.

1. What Is a Deadlock?

A deadlock is a state where two or more threads are blocked forever, each waiting for a lock held by the other. Because all involved threads are waiting, none can proceed to release the locks they hold, creating a permanent standstill.

The most common cause is a cyclic locking dependency. Imagine two threads, Thread A and Thread B, that need to acquire two locks, Lock 1 and Lock 2, to do their work.

  1. Thread A acquires Lock 1.
  2. Thread B acquires Lock 2.
  3. Thread A tries to acquire Lock 2, but it's held by Thread B, so Thread A waits.
  4. Thread B tries to acquire Lock 1, but it's held by Thread A, so Thread B waits.

Both threads are now stuck in a circular "hold-and-wait" pattern, and neither can make progress.

Deadlock Example Diagram
This diagram illustrates a classic deadlock. Thread-0 holds a lock on one object and is waiting for a lock held by Thread-1. Simultaneously, Thread-1 holds its lock and is waiting for the one held by Thread-0, creating a circular wait and causing both threads to block indefinitely.

A classic real-world example is transferring money between two bank accounts. Consider this pseudo-code:

// A wants to transfer from myAccount to yourAccount
transfer(myAccount, yourAccount, amount);

// B wants to transfer from yourAccount to myAccount
transfer(yourAccount, myAccount, amount);

void transfer(Account from, Account to, ...) {
    synchronized(from) {        // Thread A locks myAccount, Thread B locks yourAccount
        synchronized(to) {      // Thread A waits for yourAccount, Thread B waits for myAccount -> DEADLOCK!
            // ... perform transfer
        }
    }
}

If these two transfers run at the same time, they can easily deadlock.

2. Finding Deadlocks with a Thread Dump

When your application becomes unresponsive, one of the first diagnostic steps is to take a thread dump. A thread dump is a snapshot of the state of all threads inside a running Java Virtual Machine (JVM) at a specific moment. It's a plain text file that tells you what each thread was doing, including which locks it holds and which it's waiting for.

Generating and Reading a Thread Dump

First, you need the Process ID (PID) of your Java application, which you can find using the jps command. Then, you generate the dump using jstack:

# Find the PID of your Java application
$ jps
12345 YourSpringBootApplication

# Generate a thread dump for that PID and save it to a file
$ jstack -l 12345 > threaddump.txt

Now, let's understand how to interpret this file to find a deadlock.

‘Mastering Thread Dump Analysis: 9 Tips & Tricks’ Webinar

This video, ‘Mastering Thread Dump Analysis,’ provides an excellent overview of what a thread dump contains and, more importantly, shows exactly what a deadlock looks like within one.

Please watch the segment from 37:04 to 41:29. The speaker uses a great analogy of two trains on a single track to explain a deadlock and then shows how this scenario manifests in a thread dump file, highlighting the circular lock dependency.

As the video explains, when analyzing a thread dump for a deadlock, you are looking for this specific pattern:

  1. Find threads in the BLOCKED state. This tells you the thread is waiting to acquire a lock.
  2. Examine the thread's stack trace to see which lock it's waiting to lock.
  3. Look for another BLOCKED thread.
  4. Check if the second thread locked the resource the first thread is waiting for, and is waiting to lock a resource that the first thread has locked.

Most modern JVMs are smart enough to detect these simple deadlocks and will often print a "Found one Java-level deadlock" section right in the thread dump, making your job easier. It will explicitly name the deadlocked threads and show the lock cycle.

Here is an example snippet you might find in a thread dump analysis tool:

Thread Dump Analysis for Blocked Threads
This image shows the detailed stack trace of a blocked thread. Notice the thread state and the 'locked' synchronizers section. In a real deadlock, you'd cross-reference this with another thread's dump to find the circular dependency.

While manual analysis is a key skill, in production you'd likely use tools like FastThread, JProfiler, or even your IDE's profiler to automatically parse these dumps and visualize the deadlocks for you.

3. Deadlock Prevention Strategies

Detecting a deadlock is good, but preventing it is far better. In an interview, explaining these proactive strategies is crucial.

Strategy 1: Lock Ordering (The Primary Solution)

The most effective way to prevent deadlocks is to enforce a strict, global order in which all threads acquire locks. If every thread attempts to lock resources in the same sequence, a circular wait is impossible.

Let's revisit our transferMoney example. We can fix it by ensuring we always lock the Account objects in a consistent order, regardless of which is the "from" or "to" account. A simple way to create this order is by using an intrinsic, unique property of the objects.

Deadlock Prevention in Concurrent Programs

The article 'Deadlock Prevention in Concurrent Programs' provides an excellent, production-grade solution for the transferMoney problem using lock ordering.

Read the 'Mitigation' section. Pay close attention to how System.identityHashCode() is used to create a consistent ordering for acquiring locks. Also, note the clever use of a 'tie-breaking' lock to handle the rare case of a hash collision. This is a very robust solution and an impressive detail to mention in an interview.

By using the object's hash code (or another unique identifier like an account number) to decide which account to lock first, we eliminate the possibility of a deadlock. For example: always lock the account with the smaller hash code first.

Strategy 2: Lock Timeout (Timeout and Back-off)

Instead of waiting indefinitely for a lock, a thread can try to acquire it for a specified amount of time. ReentrantLock (which we discussed in the last lesson) supports this with its tryLock() method.

The strategy is as follows:

  1. A thread attempts to acquire a lock.
  2. If it can't get the lock within a timeout period, it gives up.
  3. Crucially, it must then release all locks it currently holds.
  4. It waits for a random period of time (a "back-off") and then retries the entire operation from the beginning.

Deadlock Prevention in Java

This video on 'Deadlock Prevention in Java' explains the timeout and back-off strategy in detail.

Watch the sections on 'Lock Reordering' (00:40 - 03:40) to reinforce the previous concept, and then 'Timeout Back-off' (03:40 - 11:55). The speaker provides a full code example and explains why a randomized back-off is important to avoid a 'live-lock'—another type of concurrency problem where threads are active but make no progress.

This strategy is less efficient than lock ordering because it can lead to wasted work when retrying, but it's a valid way to break deadlocks in complex systems where a global lock order is difficult to enforce.

Strategy 3: Avoid Holding Locks When Calling External Code

A subtle but critical design principle is to never call unknown or third-party code while holding a lock.

synchronized (myLock) {
    // ... my code ...
    someExternalLibrary.doSomething(); // DANGER!
    // ... my code ...
}

You don't know what doSomething() does. It might try to acquire other locks, including one that another thread is holding while waiting for myLock. This can easily introduce a deadlock that is completely outside of your control and very difficult to debug. Always release your locks before calling code you don't own.

Test your understanding! (Interview Question)

You are conducting a code review for a new social media feature that allows users to "friend" each other. The addFriend method needs to update both users' friend lists atomically. You see the following implementation:

public void addFriend(User userA, User userB) {
    // some validation logic...

    synchronized (userA) {
        synchronized (userB) {
            userA.getFriends().add(userB);
            userB.getFriends().add(userA);
        }
    }
}

What potential concurrency issue do you see here? How would you explain the risk to your colleague, and what solution would you propose?

Show answer

The Issue: This code is vulnerable to a classic deadlock.

Explanation of Risk: Imagine two users trying to friend each other at the same time.

  • Thread 1 calls addFriend(sara, david).
  • Thread 2 calls addFriend(david, sara).

With unlucky timing, Thread 1 could lock sara's user object and then try to lock david's. At the same time, Thread 2 could lock david's user object and then try to lock sara's. This creates a circular dependency where both threads are blocked, waiting for the lock held by the other. The friend requests will never complete, and the threads will be stuck forever.

Proposed Solution: The best solution is to enforce a lock ordering. We must ensure that we always lock the User objects in a consistent order. We can achieve this by using a unique, immutable property of the User object, such as its userId or even its username if they are unique.

The corrected code would look something like this:

public void addFriend(User userA, User userB) {
    // some validation logic...

    User first = userA.getId() < userB.getId() ? userA : userB;
    User second = userA.getId() < userB.getId() ? userB : userA;

    synchronized (first) {
        synchronized (second) {
            userA.getFriends().add(userB);
            userB.getFriends().add(userA);
        }
    }
}

By always locking the user with the smaller ID first, we break the circular dependency and eliminate the risk of deadlock.

Conclusion

We've covered a lot of ground today on a topic that is critical for building reliable, high-performance systems. Understanding how to both diagnose and prevent deadlocks is a hallmark of a senior engineer.

Key Takeaways:

  • Deadlock: A state where multiple threads are permanently blocked, each waiting for a resource held by another, due to a circular locking dependency.
  • Thread Dump Analysis: A thread dump is a snapshot of all JVM threads. You can find deadlocks by looking for threads in the BLOCKED state and identifying a circular locked vs. waiting to lock pattern.
  • Lock Ordering: The most robust prevention strategy. Ensure all threads acquire multiple locks in the same, globally defined order.
  • Lock Timeout: A fallback strategy where threads use tryLock() to attempt acquiring a lock, and if they fail, they release all held locks and retry after a back-off period.
  • Safe Design: Never hold a lock while calling external code whose behavior you don't control.

Next Up

So far in this module, we've focused on concurrency within the traditional "thread-per-request" model. This model has scalability limitations, especially under very high load. What if we could handle requests without tying up a thread for the entire duration of an I/O operation?

In our next lesson, we will explore a different paradigm altogether as we explain the core principles of reactive programming, including non-blocking I/O and backpressure. This will open the door to building highly scalable and resilient microservices.

Can't find a good explanation? Sign up and we'll make it for you

Sign up