1. A deep dive into deadlock
Deadlock (“mutual blocking”) is a situation where two or more threads wait for each other forever: each holds some resource and tries to acquire another one that is already held by a neighbouring thread. As a result, no one can proceed, and the program hangs. It’s like two cars on a narrow bridge, nose to nose: until one backs up—nobody moves.
In Java, a deadlock is not an exception (Exception) but a program “freeze”. Threads do not finish and do not crash with an error; they simply wait for each other indefinitely. That’s why such bugs are insidious: they appear only “under certain circumstances”.
The four conditions for a deadlock
- 1. Mutual exclusion
A resource can be held by only one thread at a time (for example, an object monitor, a file). - 2. Hold and wait
A thread holds one resource and tries to acquire a second without releasing the first. - 3. No preemption
You cannot forcibly take a resource away from a thread: only the thread itself can release it. - 4. Circular wait
There is a closed chain where each thread is waiting for a resource held by the next thread in the chain.
A deadlock is possible only if ALL these conditions hold simultaneously. Break at least one—and a deadlock cannot occur.
2. A code example of a deadlock
Scenario
- There are two resources: lock1 and lock2 (plain objects).
- Thread A first acquires lock1, then tries to acquire lock2.
- Thread B first acquires lock2, then tries to acquire lock1.
Example (do not try this at home!)
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Thread 1
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: acquired lock1");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 1: attempts to acquire lock2");
synchronized (lock2) {
System.out.println("Thread 1: acquired lock2");
}
}
});
// Thread 2
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: acquired lock2");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 2: attempts to acquire lock1");
synchronized (lock1) {
System.out.println("Thread 2: acquired lock1");
}
}
});
thread1.start();
thread2.start();
}
}
What happens when you run it?
- Thread 1 acquires lock1, thread 2 — lock2.
- Both “sleep” for 100 milliseconds, managing to acquire the first lock.
- Then each tries to acquire the second lock, which is already held by the other thread.
- Both threads wait for each other indefinitely—a deadlock occurs.
You’ll see in the console:
Thread 1: acquired lock1
Thread 2: acquired lock2
Thread 1: attempts to acquire lock2
Thread 2: attempts to acquire lock1
Why does a deadlock occur? A detailed breakdown
- Mutual exclusion: each lock can be held by only one thread.
- Hold and wait: Thread 1 holds lock1 and waits for lock2; Thread 2 holds lock2 and waits for lock1.
- No preemption: nobody can steal a lock from the outside—only by exiting the synchronized block.
- Circular wait: the wait forms a cycle between the two threads.
3. How to avoid and prevent deadlocks
Acquire resources in the same order
The key rule: if multiple threads need to acquire multiple resources—always do it in the same order. For example: first lock1, then lock2—everywhere in the code.
public void doSomething() {
Object firstLock = lock1;
Object secondLock = lock2; // single order "lock1 -> lock2"
synchronized (firstLock) {
synchronized (secondLock) {
// Work with both resources
}
}
}
With a single order, circular wait is impossible—the deadlock condition is violated.
Use tryLock with a timeout (ReentrantLock)
When it’s not known in advance which resources will be needed, use ReentrantLock and the tryLock method so you don’t wait forever.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public void doWork() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Critical section
} finally {
lock2.unlock();
}
} else {
System.out.println("Failed to acquire lock2, rolling back");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Failed to acquire lock1, rolling back");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Plus: you can roll back and try again later. Minus: the code is more complex, but a deadlock is ruled out.
Minimize how long you hold locks
Hold a lock for as little time as possible. Do only what’s necessary inside synchronized/lock; move everything else outside the critical section.
Avoid nested locking
The fewer nested synchronized/lock blocks, the lower the risk of deadlock. If possible, use a single lock for related resources.
Use ready-made thread-safe structures
The standard library already solves many tasks: collections like ConcurrentHashMap and other classes from java.util.concurrent minimize the risk of deadlock and simplify code.
4. Diagnosing deadlocks
Thread dump and jstack
A thread dump is a snapshot of all thread states in the JVM.
From the console:
jstack <pid>
Where <pid> is the Java process ID (you can obtain it via jps).
In an IDE there is usually a “Thread Dump” button on the debug panel.
In the dump, look for:
- Statuses BLOCKED or WAITING.
- Messages like waiting to lock ... and locked ....
- The JVM phrase: "Found one Java-level deadlock:" — when a deadlock is detected.
An example excerpt of a dump:
"Thread-1":
waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
which is held by "Thread-1"
This is a deadlock: the threads are holding each other.
VisualVM and other tools
- VisualVM — a free tool for thread analysis and deadlock detection.
- Java Mission Control / Flight Recorder — advanced monitoring and profiling.
In VisualVM you can inspect the thread tree, their states, and get a deadlock notification.
Table: “How not to end up in a deadlock”
| Deadlock cause | How to avoid |
|---|---|
| Acquiring resources in different orders | Always adhere to a single resource acquisition order |
| Nested synchronized | Minimize nesting; when possible, use a single lock |
| Holding a lock for too long | Reduce the work done inside the critical section |
| Using multiple locks at the same time | Use tryLock with a timeout and support rollback |
| Non-thread-safe collections | Use concurrent collections (ConcurrentHashMap and others) |
5. Common mistakes when dealing with deadlocks
Error No. 1: Acquiring resources in different orders. The most common cause is different threads taking locks in different orders. Even if “it seems faster”, stick to a single order—otherwise circular wait is guaranteed.
Error No. 2: Nested synchronized without necessity. Extra nesting increases the risk of deadlock and complicates the code. Simplify your locking model.
Error No. 3: Ignoring tryLock and timeouts. Locking via synchronized will make a thread wait indefinitely. If you’re not certain, use tryLock with a timeout and rollback logic.
Error No. 4: Long-running code inside a lock. Network requests, I/O, and heavy computations under a lock sharply increase the chance of problems. Move them outside the critical section.
Error No. 5: Not analysing thread dumps. A thread dump is your best friend when hunting deadlocks. Use jstack and analyse the BLOCKED/WAITING states and the “holding/waiting to lock” chains.
GO TO FULL VERSION