How to prevent deadlock?
Deadlock can be prevented by violating at least one of the 4 Coffman conditions. The key idea: deadlock requires all 4 conditions to hold simultaneously, so violating any one of...
Deadlock can be prevented by violating at least one of the 4 Coffman conditions. The key idea: deadlock requires all 4 conditions to hold simultaneously, so violating any one of them makes deadlock impossible.
Unlike detecting deadlock (file 19), which only diagnoses the problem, this file describes preventive strategies — how to design a system so that deadlock never occurs.
Junior Level
Basic Understanding
Deadlock can be prevented by violating at least one of the 4 Coffman conditions. The most effective method — Lock Ordering.
Method 1: Lock Ordering
Always acquire resources in a strictly defined order:
public class BankAccount {
private final int id; // Unique ID for determining order
private double balance;
public BankAccount(int id, double balance) {
this.id = id;
this.balance = balance;
}
// Safe money transfer — always in ID order
public static void transfer(BankAccount from, BankAccount to, int amount) {
// Determine order by ID
BankAccount first = from.id < to.id ? from : to;
BankAccount second = from.id < to.id ? to : from;
synchronized(first) {
synchronized(second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
private void withdraw(int amount) { balance -= amount; }
private void deposit(int amount) { balance += amount; }
}
Method 2: tryLock with Timeout
Use ReentrantLock.tryLock() instead of synchronized:
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
public void safeOperation() throws InterruptedException {
while (true) {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Both acquired — work
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock(); // Always release the first!
}
}
// Failed — pause and retry
Thread.sleep(50);
}
}
Method 3: One Global Lock
// Simple, but slow
private final Object globalLock = new Object();
public void operation1() {
synchronized(globalLock) {
// All operations go through one lock — deadlock impossible
}
}
public void operation2() {
synchronized(globalLock) {
// No nested locks → no deadlock
}
}
Why it’s slow: all threads perform operations sequentially, even if they don’t conflict (e.g., operation1 reads data while operation2 writes to a different file). This reduces multithreading to single-threading. Use only when operations truly must be strictly sequential.
Middle Level
Lock Ordering with hashCode
When resources don’t have a natural ordering:
public void safeOperation(Object resource1, Object resource2) {
// Order by identity hash code
int h1 = System.identityHashCode(resource1);
int h2 = System.identityHashCode(resource2);
if (h1 < h2) {
synchronized(resource1) {
synchronized(resource2) { /* ... */ }
}
} else if (h1 > h2) {
synchronized(resource2) {
synchronized(resource1) { /* ... */ }
}
} else {
// HashCode collision — use a global tie-breaker
synchronized(globalLock) {
synchronized(resource1) {
synchronized(resource2) { /* ... */ }
}
}
}
}
Try-Lock with Rollback
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
public boolean tryTransfer(Account from, Account to, int amount) {
if (!lockA.tryLock()) return false;
try {
if (!lockB.tryLock()) return false; // Failed — release A
try {
from.withdraw(amount);
to.deposit(amount);
return true;
} finally {
lockB.unlock();
}
} finally {
lockA.unlock();
}
}
This violates Hold and Wait — if the second resource cannot be acquired, we release the first.
Lock-Free Data Structures
The best way to prevent deadlock — don’t use locks:
// Instead of synchronized HashMap:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
// Use ConcurrentHashMap — lock-free:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// Instead of AtomicInteger + synchronized:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // CAS — no lock → no deadlock
Comparing Approaches
| Method | Pros | Minors | When to Use |
|---|---|---|---|
| Lock Ordering | Reliable, simple | Need resource ordering | Most cases |
| Try-Lock | Flexible, with timeout | Needs retry loop | When order is unknown |
| One lock | Simple | Low parallelism | Simple systems |
| Lock-free | No deadlock at all | Harder to implement | High load |
Senior Level
Under the Hood: Violating Coffman Conditions
| Method | Which Condition Is Violated | Mechanism |
|---|---|---|
| Lock Ordering | Circular Wait | Impossible to create cycle |
| Try-Lock + release | Hold and Wait | Don’t hold while waiting |
| lockInterruptibly | No Preemption | Can be interrupted |
| Lock-free/Atomic | Mutual Exclusion | No exclusive ownership |
| One global lock | Circular Wait | No nesting |
Thread-Specific Resources (Sharding)
Design the system so that threads don’t share resources:
// Each thread works with its own segment
public class ShardedCounter {
private final Map<Integer, AtomicInteger> shards = new ConcurrentHashMap<>();
public void increment(int userId) {
int shard = userId % 16; // 16 shards
shards.computeIfAbsent(shard, k -> new AtomicInteger(0))
.incrementAndGet();
}
}
// Threads with different userIds never compete → no deadlock
Verification: Static Analysis
ThreadSanitizer (TSan)
# For Java via LLVM
java -agentpath:/path/to/libtsan.so MyApp
Finds potential races and deadlocks even at the testing stage.
SonarQube
Detects nested synchronized blocks in different orders:
// SonarQube will warn:
public void method1() {
synchronized(lockA) {
synchronized(lockB) { // Potential deadlock
}
}
}
public void method2() {
synchronized(lockB) {
synchronized(lockA) { // Reverse order!
}
}
}
Edge Cases
Deadlock with External Systems
Java thread: holds lock → waits for DB response
DB: holds table lock → waits for data from Java (via callback)
→ Deadlock between Java and DB!
Solution: don’t hold Java locks during I/O operations.
// BAD:
synchronized(lock) {
database.update(); // Holding lock during I/O
}
// GOOD:
Object result = database.update(); // I/O without lock
synchronized(lock) {
// Update state
}
Interruption and synchronized
// synchronized CANNOT be interrupted:
synchronized(lock) {
// If deadlock — thread will hang until JVM restart
}
// ReentrantLock CAN be interrupted:
lock.lockInterruptibly();
try {
// If deadlock — can call thread.interrupt()
} finally {
lock.unlock();
}
Diagnostics and Monitoring
ThreadMXBean
public class DeadlockMonitor {
public static void checkAndAlert() {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlocked = mxBean.findDeadlockedThreads();
if (deadlocked != null) {
for (long threadId : deadlocked) {
ThreadInfo info = mxBean.getThreadInfo(threadId, true, true);
log.error("DEADLOCK: Thread {} blocked on {}",
info.getThreadName(),
info.getLockName());
}
// Send alert, restart, etc.
}
}
}
Warning: This is an expensive operation — don’t run it too often (every 30+ seconds).
LockWaitTime Metrics
// Monitor lock acquisition time
long start = System.nanoTime();
lock.lock();
long waitTime = System.nanoTime() - start;
if (waitTime > 1_000_000) { // > 1ms
log.warn("High lock wait time: {} ms", waitTime / 1_000_000);
}
// Sudden increase = harbinger of deadlock or contention
Best Practices
- Lock Ordering — the most reliable method, always use for nested locks
- tryLock(timeout) — to avoid eternal waiting
- Avoid locks during I/O — don’t hold locks during DB/API requests
- Lock-free structures — ConcurrentHashMap, Atomic* instead of synchronized
- ReentrantLock instead of synchronized — for interruptibility
- Sharding — threads with independent data = no deadlock
- Monitor LockWaitTime — growth = harbinger of problems
- Static Analysis — SonarQube will find nested locks at development stage
When NOT to Use Prevention Strategies
- One lock, one thread — if you have only one lock and one thread, deadlock is impossible by definition, no need to complicate
- Read-only operations — if all threads only read data (ReadLock or immutable), Mutual Exclusion does not apply, deadlock is impossible
- Prototype/MVP for 1-2 days — a global lock is fine, refactor when real load appears
- Actor model (Akka) — if you already use actors, lock-level deadlock prevention is not applicable — actors don’t share state
Lock Ordering vs Try-Lock: What to Choose?
| Situation | Choice | Why |
|---|---|---|
| Fixed set of resources (DB, files) | Lock Ordering | Order known in advance, simple and reliable |
| Dynamic resources (created on the fly) | Try-Lock | Order unknown, tryLock is more flexible |
| High load + known resources | Lock Ordering | Zero overhead on acquisition |
| Need to avoid eternal waiting | Try-Lock + timeout | Lock Ordering can wait forever if there’s a bug in ordering |
| Legacy code with nested synchronized | Try-Lock | Rewriting to ordering is harder than replacing synchronized with tryLock |
Interview Cheat Sheet
Must know:
- Deadlock is prevented by violating at least one of the 4 Coffman conditions
- Lock Ordering — violates Circular Wait — the most reliable and free method
- TryLock + release on failure — violates Hold and Wait
- ReentrantLock instead of synchronized — violates No Preemption (interrupt support)
- Lock-free/Atomic — violates Mutual Exclusion
- Don’t hold locks during I/O operations — this leads to deadlock with external systems (DB, API)
Frequent follow-up questions:
- What to choose: Lock Ordering or Try-Lock? — Lock Ordering for a fixed set of resources (zero overhead), Try-Lock for dynamic resources or legacy code
- How does SonarQube help prevent deadlock? — Finds nested synchronized blocks in different orders at development stage
- What is sharding as a prevention method? — Threads work with independent data segments, no competition = no deadlock
Red flags (DO NOT say):
- “One global lock is a good solution for production” — kills parallelism, only for prototypes
- “TryLock guarantees no deadlock” — TryLock prevents eternal waiting, but deadlock pattern is still possible with incorrect retry logic
- “Deadlock prevention is not needed for web applications” — web applications also use DB, caches, external APIs
- “ReentrantLock solves all deadlock problems” — it only provides more tools, deadlock is still possible with misuse
Related topics:
- [[19. What conditions are necessary for deadlock to occur]]
- [[21. What is race condition]]
- [[22. How to avoid race condition]]
- [[23. What are Virtual Threads in Java 21]]
- [[27. What is the difference between Thread and Runnable]]