Deadlock in Java
A deadlock leaves work stuck because each participant is waiting for something another blocked participant holds. In Java, two threads acquiring two locks in opposite orders are a common demonstration. We will examine that dependency and how to avoid creating it.
What is Deadlock?
A deadlock occurs when two or more threads are permanently waiting for resources or locks held by each other.
For example:
Thread-1
|
| holds Lock-A
|
+------> waiting for Lock-B
^
|
holds Lock-B
|
Thread-2
|
+------> waiting for Lock-ANeither thread can continue because:
- Thread-1 is waiting for Thread-2 to release Lock-B.
- Thread-2 is waiting for Thread-1 to release Lock-A.
As a result, both threads remain blocked.
Simple Example of Deadlock
Consider two objects:
Object lock1 = new Object();
Object lock2 = new Object();Now suppose two threads acquire these locks in opposite orders.
Thread-1
synchronized (lock1) {
synchronized (lock2) {
// Work
}
}Thread-2
synchronized (lock2) {
synchronized (lock1) {
// Work
}
}This can create a deadlock.
How Deadlock Happens
Suppose Thread-1 starts first:
Thread-1
↓
Acquires lock1Then Thread-2 starts:
Thread-2
↓
Acquires lock2Now Thread-1 tries to acquire lock2:
Thread-1
↓
Waiting for lock2At the same time, Thread-2 tries to acquire lock1:
Thread-2
↓
Waiting for lock1The situation becomes:
Thread-1
↓
holds lock1
↓
waiting for lock2
↑
|
holds lock2
|
Thread-2
↓
waiting for lock1Neither thread can proceed.
This is a deadlock.
Complete Deadlock Example
public class HelloWorld {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println(
"Thread-1 acquired lock1"
);
synchronized (lock2) {
System.out.println(
"Thread-1 acquired lock2"
);
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println(
"Thread-2 acquired lock2"
);
synchronized (lock1) {
System.out.println(
"Thread-2 acquired lock1"
);
}
}
});
thread1.start();
thread2.start();
}
}Depending on thread scheduling, the program can reach a state where:
Thread-1 → holds lock1 → waits for lock2
Thread-2 → holds lock2 → waits for lock1At that point, neither thread can continue.
Why Does Deadlock Occur?
Deadlock generally occurs when threads acquire multiple locks in an inconsistent order.
For example:
Thread-1:
Lock-A → Lock-B
Thread-2:
Lock-B → Lock-AThis creates the possibility of circular waiting.
A safer approach is to use the same lock acquisition order:
Thread-1:
Lock-A → Lock-B
Thread-2:
Lock-A → Lock-BNow one thread may have to wait for the first lock, but the circular dependency is avoided.
Four Necessary Conditions for Deadlock
A classic deadlock analysis identifies four conditions that must exist together for a deadlock to occur.
These are:
- Mutual exclusion
- Hold and wait
- No preemption
- Circular wait
These conditions are often called the Coffman conditions.
1. Mutual Exclusion
A resource can be held by only one thread at a time.
For example, a monitor lock can be owned by only one thread at a time.
Lock-A
↓
Thread-1 owns the lockAnother thread cannot simultaneously own that same monitor.
2. Hold and Wait
A thread holds one resource while waiting for another resource.
For example:
Thread-1
↓
holds Lock-A
↓
waits for Lock-BThe thread does not release Lock-A while waiting for Lock-B.
3. No Preemption
A resource cannot simply be forcibly taken away from the thread that owns it.
The thread generally releases the monitor when it exits the synchronized region.
For example:
synchronized (lock) {
// Thread owns lock
}The monitor is released when the synchronized block is exited.
4. Circular Wait
A circular dependency exists between threads.
For example:
Thread-1
↓
waiting for Lock-B
↑
|
held by
|
Thread-2
↓
waiting for Lock-A
↑
|
held by
|
Thread-1This creates a cycle:
Thread-1 → Lock-B → Thread-2 → Lock-A → Thread-1The threads cannot make progress.
Deadlock Example Using Methods
Deadlock does not require explicit synchronized blocks. It can also occur with synchronized methods.
class Resource {
public synchronized void methodA(Resource other) {
System.out.println(
Thread.currentThread().getName()
+ " entered methodA"
);
other.methodB();
}
public synchronized void methodB() {
System.out.println(
Thread.currentThread().getName()
+ " entered methodB"
);
}
}Now:
Resource resource1 = new Resource();
Resource resource2 = new Resource();One thread can call:
resource1.methodA(resource2);while another calls:
resource2.methodA(resource1);A deadlock can occur if:
- Thread-1 holds resource1's monitor
- Thread-2 holds resource2's monitor
- Thread-1 waits for resource2's monitor
- Thread-2 waits for resource1's monitor
Deadlock vs Race Condition
Deadlock and race condition are different problems.
| Deadlock | Race Condition |
|---|---|
| Threads wait indefinitely | Result depends on timing/interleaving |
| Threads may stop making progress | Threads may continue but produce incorrect results |
| Usually involves conflicting resource acquisition | Usually involves unsafe concurrent access to shared state |
| Can cause application operations to hang | Can cause inconsistent data |
| Often caused by circular lock dependencies | Often caused by missing synchronization or inadequate atomicity |
Deadlock
Thread-1 → waits for Thread-2
Thread-2 → waits for Thread-1Race Condition
Thread-1 ─┐
├→ shared data
Thread-2 ─┘
Timing affects the resultDeadlock vs Starvation
Deadlock is also different from starvation.
Deadlock
Threads are waiting for each other in a cycle.
Thread-1 → waits for Thread-2
Thread-2 → waits for Thread-1Starvation
A thread cannot obtain the resources or CPU time it needs because other threads continually get access first.
Thread-1 → repeatedly gets resource
Thread-2 → keeps waitingThe thread may eventually proceed, but it is not receiving a fair opportunity to do so.
Deadlock vs Livelock
Another related problem is livelock.
Deadlock
Threads are blocked and cannot continue.
Thread-1 → waiting
Thread-2 → waitingLivelock
Threads are active and repeatedly responding to each other, but no useful progress is made.
Conceptually:
Thread-1 → changes state
Thread-2 → responds
Thread-1 → changes again
Thread-2 → responds againThe threads are running, but the application is not making useful progress.
How to Prevent Deadlock
There is no single universal technique, but several design practices can significantly reduce deadlock risk.
1. Always Acquire Locks in the Same Order
This is one of the most common approaches.
Suppose an application uses:
Lock-A
Lock-BAlways acquire them in the same order:
Lock-A
↓
Lock-BFor example:
synchronized (lockA) {
synchronized (lockB) {
// Work
}
}Every thread that needs both locks should follow the same order.
Avoid:
synchronized (lockB) {
synchronized (lockA) {
// Work
}
}when another code path acquires lockA before lockB.
2. Avoid Unnecessary Nested Locks
The more locks a thread holds simultaneously, the more complicated lock dependencies become.
Instead of:
synchronized (lockA) {
synchronized (lockB) {
synchronized (lockC) {
// Work
}
}
}consider whether all three locks are actually necessary at the same time.
Reducing the number of simultaneously held locks can simplify concurrency design.
3. Keep Synchronized Sections Small
Avoid unnecessarily large synchronized sections.
Instead of:
synchronized (lock) {
// Large amount of unrelated work
// Shared resource access
// More unrelated work
}protect only the critical section when appropriate:
// Non-critical work
synchronized (lock) {
// Shared resource access
}
// More non-critical workThis can reduce lock contention and make lock ownership easier to understand.
4. Avoid Calling External Code While Holding a Lock
Be careful when calling methods that you do not control while holding a lock.
For example:
synchronized (lock) {
externalService.process();
}If externalService.process() indirectly waits for another lock, the lock dependency can become difficult to predict.
Holding locks only when necessary can reduce this risk.
5. Use a Consistent Locking Strategy
A large application should have a clear approach to lock ownership.
For example:
Resource A → Lock-A
Resource B → Lock-BRequired order:
Lock-A → Lock-BFollowing a consistent strategy makes concurrent code easier to reason about.
6. Use tryLock() When Appropriate
The Lock API provides methods that can attempt to acquire a lock without waiting indefinitely.
For example:
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// Work
} finally {
lock.unlock();
}
} else {
// Could not acquire lock
}Unlike an ordinary blocking lock acquisition, tryLock() can allow the application to choose what to do when the lock is unavailable.
Timed versions are also available.
Deadlock Prevention Using tryLock()
Consider two locks:
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();A thread can attempt to acquire both:
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
// Work with both locks
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}A more complete design may use timed tryLock() calls and release already-acquired locks when the second lock cannot be obtained.
This approach can prevent a thread from waiting indefinitely for the second lock.
Deadlock Detection
Deadlocks can sometimes be detected using Java monitoring and diagnostic tools.
For example, Java's thread dump mechanisms can reveal threads waiting on monitors or locks.
A thread dump may show information such as:
Thread-1:
waiting for lock held by Thread-2
Thread-2:
waiting for lock held by Thread-1This can help identify a circular dependency.
Java diagnostic tools such as jstack can be useful when investigating deadlocks in running JVM applications.
Example of a Deadlock Scenario
Consider two bank accounts:
Account A
Account BA transfer from A to B might lock them in this order:
Lock Account A
Lock Account B
Transfer money
Unlock Account B
Unlock Account AAt the same time, another transfer from B to A might lock them in the opposite order:
Lock Account B
Lock Account A
Transfer money
Unlock Account A
Unlock Account BThis can create:
Transfer 1
↓
holds A
↓
waits for B
Transfer 2
↓
holds B
↓
waits for AA common solution is to establish a consistent ordering for account locks.
For example: Always lock the account with the lower identity/order first.
Then both transfers follow the same lock ordering.
Deadlock and synchronized
Deadlock can occur when using synchronized.
For example:
synchronized (lockA) {
synchronized (lockB) {
// Work
}
}The problem is not that synchronized itself is incorrect.
The problem is how multiple locks are acquired by different threads.
Good synchronization design is therefore important.
Deadlock and ReentrantLock
Deadlock can also occur when using ReentrantLock.
For example:
lockA.lock();
try {
lockB.lock();
try {
// Work
} finally {
lockB.unlock();
}
} finally {
lockA.unlock();
}If another thread acquires lockB first and then waits for lockA, the same circular waiting problem can occur.
Using Lock does not automatically eliminate deadlocks.
Important: synchronized Does Not Automatically Cause Deadlock
It is incorrect to say:
"synchronized causes deadlock."The synchronized mechanism provides controlled access to monitors.
Deadlock occurs because of a lock dependency pattern in which threads wait for resources held by each other.
For example:
Thread-1 → Lock-A → waits for Lock-B
Thread-2 → Lock-B → waits for Lock-AThe problem is the circular dependency.
How to Identify Deadlock in an Application
Common symptoms include:
- Certain operations stop progressing.
- Threads remain blocked for a long time.
- CPU usage may be low even though requests are not completing.
- Thread dumps show threads waiting for locks held by each other.
- Requests may remain stuck indefinitely.
- The application may appear responsive in some areas while specific operations remain blocked.
These symptoms alone do not prove a deadlock. Thread dumps and lock information should be used to confirm the condition.
Simple Deadlock Prevention Pattern
A useful design rule is:
Multiple Locks
↓
Define an Order
↓
Always Acquire
in That Order
↓
Release Locks
↓
ContinueFor example:
Lock-A
↓
Lock-B
↓
Critical Section
↓
Unlock-B
↓
Unlock-AEvery thread that requires both locks should follow the same ordering.
Key Takeaways
- Deadlock occurs when threads wait indefinitely for resources or locks held by each other.
- Deadlock commonly involves multiple locks.
- A circular dependency between threads can cause deadlock.
- The four classic deadlock conditions are: mutual exclusion, hold and wait, no preemption, and circular wait.
- Deadlock can occur with both synchronized and Lock APIs.
- Consistent lock ordering is one of the common ways to prevent circular wait.
- Avoid unnecessary nested locks.
- Keep critical sections appropriately small.
- Be careful when calling external code while holding locks.
- tryLock() can be useful when indefinite waiting should be avoided.
- Thread dumps can help diagnose deadlocks.
- Deadlock is different from race condition, starvation, and livelock.
- synchronized itself does not automatically cause deadlock; unsafe lock dependencies do.
Deadlock in One Diagram
Thread-1
|
| holds
v
Lock-A
|
| waiting for
v
Lock-B
^
| holds
|
Thread-2
|
| waiting forv Lock-A
Circular dependency ↓ DEADLOCK
The key idea is: A deadlock occurs when threads become permanently blocked because each thread is waiting for a resource held by another thread in the same dependency cycle.
See you in next page....🙂