Hello! Welcome to your next lesson in the Advanced Java Concurrency module.
In our previous lesson, we explored the producer-consumer pattern and saw how a high-level utility like BlockingQueue can manage thread coordination for us, preventing many common concurrency issues. However, in large, complex systems, you'll inevitably encounter situations where you need to manage locks manually using lower-level primitives like synchronized or ReentrantLock. This power comes with risks, and the most significant one is deadlock.
Today, we will tackle one of the most critical and frequently asked topics in senior-level Java interviews. Your learning outcome is to explain the causes of deadlock and demonstrate a prevention strategy like lock ordering. Mastering this topic is essential for writing robust concurrent code and for proving your depth of knowledge in interviews.
1. What is a Deadlock?
A deadlock is a state in which two or more threads are stuck waiting for each other to release a resource, and none of them can proceed. The entire set of threads becomes frozen.
Imagine two threads, ThreadA and ThreadB, and two resources, Lock1 and Lock2.
ThreadAacquiresLock1.ThreadBacquiresLock2.ThreadAnow tries to acquireLock2, but it's held byThreadB, soThreadAwaits.ThreadBtries to acquireLock1, but it's held byThreadA, soThreadBwaits.
Both threads are now waiting for a lock held by the other. This circular dependency means neither can ever make progress. The diagram below illustrates this exact scenario in a hypothetical e-commerce application.

2. The Four Necessary Conditions for Deadlock
For a deadlock to occur, four conditions—often called the Coffman conditions—must be met simultaneously. In an interview, being able to list and explain these four conditions demonstrates a fundamental understanding of the problem.
Deadlock in Multithreading: Concepts, Prevention...
To understand the root causes of deadlock, let's review the four formal conditions that must exist for one to occur. This article provides a clear and concise explanation of each.
Please read the section 'Four Conditions for Deadlock'. Focus on understanding the definition of each condition: Mutual Exclusion, Hold and Wait, No Preemption, and Circular Wait.
To summarize, the four conditions are:
- Mutual Exclusion: At least one resource must be held in a non-shareable mode. In Java, this is the case with locks (
synchronizedblocks,ReentrantLock), as only one thread can own a lock at a time. - Hold and Wait: A thread is holding at least one resource and is waiting to acquire additional resources held by other threads.
- No Preemption: A resource cannot be forcibly taken from a thread. The thread must release it voluntarily. Java locks follow this rule.
- Circular Wait: A set of waiting threads {T1, T2, ..., Tn} must exist such that T1 is waiting for a resource held by T2, T2 is waiting for a resource held by T3, and so on, with Tn waiting for a resource held by T1.
If you can prevent just one of these four conditions, you can prevent deadlocks.
3. Deadlock in Action: A Code Demonstration
Let's see how easily this can happen in code. The most common interview example is a money transfer between two bank accounts.
How detect and resolve DeadLocks in Java
This video provides an excellent, clear definition of deadlock and illustrates it with a simple code example involving two threads and two locks. It perfectly visualizes the problem we are discussing.
Watch the video from the beginning to 1:47. Pay close attention to how the two threads, thread1 and thread2, attempt to acquire the locks in a different order, leading to the deadlock scenario.
Here's a similar code example, structured for clarity, which demonstrates a deadlock during a bank transfer.
public class DeadlockExample {
public static void main(String[] args) {
final Account account1 = new Account(1000);
final Account account2 = new Account(2000);
// Thread 1: Transfers from account1 to account2
Thread t1 = new Thread(() -> {
transfer(account1, account2, 100);
});
// Thread 2: Transfers from account2 to account1
Thread t2 = new Thread(() -> {
transfer(account2, account1, 200);
});
t1.start();
t2.start();
}
// This method is prone to deadlock
public static void transfer(Account from, Account to, double amount) {
System.out.println(Thread.currentThread().getName() + " trying to lock " + from);
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " locked " + from);
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " trying to lock " + to);
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " locked " + to);
// Perform transfer logic...
}
}
}
}
// A simple Account class to act as our lockable resource
class Account {
private int balance;
public Account(int balance) { this.balance = balance; }
// ... other methods
}
If you run this code, it will likely hang. t1 locks account1 and waits for account2, while t2 locks account2 and waits for account1—a classic circular wait.
How do we detect this in a real application?
In a production environment, you wouldn't just see the program hang. You would use tools to diagnose the issue. The most common method is to take a thread dump using a command-line tool like jstack or by sending a kill -3 signal to the Java process. The JVM is smart enough to analyze the thread states and will explicitly report "Found 1 deadlock." in the dump, pointing out which threads are blocked and which locks they are waiting for.
4. Prevention Strategy: Lock Ordering
The most robust and common strategy to prevent deadlocks is to break the Circular Wait condition. You can achieve this by ensuring that every thread in your application acquires locks in the same, globally consistent order.
Let's fix our bank transfer example. We need a way to decide the locking order that is consistent regardless of which account is "from" and which is "to". An easy way to do this is to use a unique, immutable property of the objects, like their hashCode() or an ID field.
Deadlock in Multithreading: Concepts, Prevention...
This article demonstrates exactly how to fix the bank transfer deadlock using resource ordering. The code example is a perfect illustration of the principle.
Read the section '1. Resource Ordering'. Study the corrected run() method in the code block. Notice how it uses hashCode() to decide which account to lock first, ensuring a consistent order.
Here is the key change to the transfer method to make it deadlock-free:
public static void transfer(Account from, Account to, double amount) {
// Establish a consistent lock order.
Account firstLock = from.hashCode() < to.hashCode() ? from : to;
Account secondLock = from.hashCode() < to.hashCode() ? to : from;
System.out.println(Thread.currentThread().getName() + " trying to lock " + firstLock);
synchronized (firstLock) {
System.out.println(Thread.currentThread().getName() + " locked " + firstLock);
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " trying to lock " + secondLock);
synchronized (secondLock) {
System.out.println(Thread.currentThread().getName() + " locked " + secondLock);
// Perform transfer logic...
}
}
}
With this change, both t1 and t2 will attempt to lock the account with the smaller hashCode first. One thread will succeed and acquire both locks, complete its transfer, and release them. The other thread will wait patiently and then acquire the locks in the same order. The circular wait is broken, and deadlock is averted.
Test your understanding!
A classic interview problem that illustrates deadlock is the Dining Philosophers Problem. The setup is simple: five philosophers sit around a table with five chopsticks, one between each pair of philosophers. A philosopher needs two chopsticks (the one on their left and the one on their right) to eat.
Imagine each philosopher is a thread, and each chopstick is a lock. A naive solution is for each philosopher to:
- Pick up their left chopstick.
- Pick up their right chopstick.
- Eat.
- Put both chopsticks down.
What specific condition could arise that leads to a deadlock? How could you apply the lock ordering strategy to solve it?
Show answer
A deadlock occurs if every philosopher simultaneously picks up their left chopstick. At this point, every philosopher is holding one chopstick and is waiting for their right chopstick. But the right chopstick for any given philosopher is the left chopstick of their neighbor, which is already being held. This creates a circular wait, and no one can eat.
To solve this with lock ordering, you can break the symmetry. For example, you can designate the chopsticks with an ID (1 through 5). The rule would be: "Always pick up the chopstick with the lower ID first."
All philosophers would follow this rule, except for one. For the philosopher sitting between chopstick 5 and 1, they would try to grab chopstick 1 first (lower ID). However, to truly break the cycle, a common solution is to have one philosopher (say, the last one) pick up their chopsticks in the reverse order (right then left). This ensures that at least one philosopher can always acquire both chopsticks, breaking the circular dependency.
Conclusion
In this lesson, we dissected the dangerous-but-critical topic of deadlocks. For your senior-level interviews, being able to articulate these concepts clearly is a strong signal of your expertise in concurrency.
Key Takeaways:
- Deadlock Definition: A state where multiple threads are blocked indefinitely, each waiting for a resource held by another.
- The Four Conditions: You should be able to name and explain Mutual Exclusion, Hold and Wait, No Preemption, and Circular Wait.
- Primary Prevention Strategy: The most effective way to prevent deadlocks is to break the Circular Wait condition by enforcing a strict, global lock ordering.
- Demonstration: Be ready to code a simple deadlock scenario (like a bank transfer) and then fix it by implementing lock ordering (e.g., using object IDs or hash codes to determine the order).
- Practical Diagnosis: Remember that in the real world,
jstackand thread dumps are your primary tools for finding deadlocks in a running application.
Preview of the Next Lesson:
So far in this module, we've dealt with threads that block—waiting for a lock to be released or for an item to appear in a queue. This model is powerful but has limitations, especially in I/O-heavy applications. In our next lesson, we will explore a more modern, non-blocking paradigm by learning how to implement asynchronous, non-blocking operations using CompletableFuture.