Question 19 · Section 9

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...

Language versions: English Russian Ukrainian

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

  1. Lock Ordering — the most reliable method, always use for nested locks
  2. tryLock(timeout) — to avoid eternal waiting
  3. Avoid locks during I/O — don’t hold locks during DB/API requests
  4. Lock-free structures — ConcurrentHashMap, Atomic* instead of synchronized
  5. ReentrantLock instead of synchronized — for interruptibility
  6. Sharding — threads with independent data = no deadlock
  7. Monitor LockWaitTime — growth = harbinger of problems
  8. 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]]