Understanding and Solving Deadlocks in Java


Understanding and Solving Deadlocks in Java are one of the most common problems in concurrent programming. They occur when multiple threads are waiting indefinitely for resources held by each other. In Java, this can happen with both synchronized
blocks and ReentrantLock
.
In this article, we will explore examples of deadlocks and their solutions in detail.
Deadlock Problem Using Synchronized
In this first example, which demonstrates a deadlock using synchronized
, imagine a scenario where two threads transfer money between two accounts in opposite directions. Below is a simplified version of the code that can lead to a deadlock:
public static void transfer(Account from, Account to, int amount) {
synchronized (from) {
// Simulate delay
Thread.sleep(100);
synchronized (to) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
Explanation:
Thread-1 locks AccountA and waits to lock AccountB.
Thread-2 locks AccountB and waits to lock AccountA.
Both threads are now blocked indefinitely, creating a deadlock.
Solving Deadlock Problem Using LockOrder
One effective way to prevent deadlocks is to enforce a consistent global lock order. For instance, we can compare account names and always acquire the lock on the account with the "lower" name first. This ensures that all threads follow the same locking sequence, eliminating the possibility of circular waiting.
public static void transfer(Account from, Account to, int amount) {
Account firstLock, secondLock;
if (from.getName().compareTo(to.getName()) < 0) {
firstLock = from;
secondLock = to;
} else {
firstLock = to;
secondLock = from;
}
synchronized (firstLock) {
synchronized (secondLock) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
Explanation:
Locks are always acquired in the same order.
Circular waiting is prevented.
Deadlock cannot occur with this strategy.
Deadlock Problem Using ReentrantLock
In this example, we can observe a deadlock occurring with ReentrantLock
. Even when using ReentrantLock
, deadlocks can happen if lock()
is called directly without a proper lock ordering or a timeout:
from.getLock().lock();
try {
Thread.sleep(100);
to.getLock().lock();
try {
from.withdraw(amount);
to.deposit(amount);
} finally {
to.getLock().unlock();
}
} finally {
from.getLock().unlock();
}
Explanation:
Thread-1 locks AccountA and waits for AccountB.
Thread-2 locks AccountB and waits for AccountA.
Both threads block indefinitely, causing deadlock.
Solving Deadlock Problem Using ReentrantLock
Solving Deadlock with ReentrantLock
and tryLock()
We can solve this by using tryLock(timeout)
with a retry loop. If a lock cannot be acquired, the thread releases any held locks and retries.
while (true) {
if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
from.withdraw(amount);
to.deposit(amount);
break; // transfer successful
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
Thread.sleep(50); // backoff before retry
}
Explanation:
The thread attempts to acquire both locks within a timeout.
If unsuccessful, it releases any locks held and retries.
This prevents indefinite waiting and ensures progress.
Deadlock is eliminated because no thread waits forever. Conclusion Deadlocks can silently freeze concurrent applications, but they can be prevented with proper techniques:
With
synchronized
, enforce a consistent lock ordering.With
ReentrantLock
, usetryLock()
with timeouts and retries. Understanding and applying these techniques is essential for building robust multithreaded Java applications.
Conclusion
Deadlocks are a common challenge in concurrent programming that can silently freeze applications if not handled properly. Through the examples presented, we have seen how both synchronized
and ReentrantLock
can lead to deadlocks when locks are acquired inconsistently or without proper management.
The key takeaways are:
With
synchronized
, enforcing a consistent global lock order ensures that threads always acquire locks in the same sequence, preventing circular waiting.With
ReentrantLock
, usingtryLock()
with timeouts and retry logic prevents threads from waiting indefinitely and guarantees progress.
By understanding these patterns and applying these strategies, developers can build robust, deadlock-free multithreaded Java applications, ensuring reliable and efficient concurrent execution.
You can check this code on github
Subscribe to my newsletter
Read articles from Luis Gustavo Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
