Senior 7 min · March 09, 2026

Java Deadlock — The Silent Freeze of Payment Service

Deadlock caused gradual latency and stall: payment service's Account and Ledger locks circular wait.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Deadlock occurs when two or more threads each hold a lock and wait for another's lock, creating a circular wait
  • Four Coffman conditions must hold: mutual exclusion, hold-and-wait, no preemption, circular wait
  • Prevention strategies: lock ordering (global hierarchy), tryLock with timeout, avoid nested locks
  • Production insight: a deadlock freezes threads silently – the app stays alive but stops making progress
  • Common tool: jstack thread dump reveals deadlocks via JVM's built-in detection
  • Biggest mistake: assuming depends_on or synchronized alone prevents deadlocks
Plain-English First

Think of Deadlock in Java — Causes and Prevention as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks into place. Imagine a narrow one-way bridge where two cars meet from opposite directions. Neither can move forward because the other is in the way, and neither can back up. In Java, this happens when Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Everyone is stuck, waiting for a resource that will never be released.

Deadlock in Java — Causes and Prevention is a fundamental concept in Java development. Understanding it will make you a more effective developer by allowing you to write high-concurrency applications that are robust and 'liveness' guaranteed. A deadlock is essentially a state where a set of processes are blocked because each process is holding a resource and waiting for another resource acquired by some other process.

In this guide, we'll break down exactly what Deadlock in Java — Causes and Prevention is, why it occurs due to specific resource-sharing patterns, and how to use it correctly in real projects to avoid system freezes. We will explore the architectural patterns that make code inherently 'deadlock-proof' and look at the diagnostic tools available in the JDK to unmask these silent performance killers.

By the end, you'll have both the conceptual understanding and practical code examples to use Deadlock in Java — Causes and Prevention with confidence in any io.thecodeforge production environment.

What Is Deadlock in Java — Causes and Prevention and Why Does It Exist?

Deadlock in Java — Causes and Prevention is a core feature of Concurrency. It isn't a designed feature, but rather a catastrophic state resulting from poor synchronization logic. It occurs when four conditions (known as the Coffman conditions) are met simultaneously:

  1. Mutual Exclusion: Only one thread can hold a resource at a time.
  2. Hold and Wait: A thread holding at least one resource is waiting to acquire additional resources held by other threads.
  3. No Preemption: Resources cannot be forcibly taken from a thread; they must be released voluntarily.
  4. Circular Wait: A closed chain of threads exists where each thread holds a resource needed by the next thread in the chain.

Understanding the problem it solves—which is actually the prevention of data corruption through locking—is the key to knowing when and how to use locking strategies effectively without halting your JVM. At io.thecodeforge, we emphasize that locks are for protecting shared state, not just for the sake of synchronization.

io/thecodeforge/concurrency/DeadlockDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package io.thecodeforge.concurrency;

/**
 * io.thecodeforge: Classic Deadlock Scenario
 * Demonstrates circular wait between two resources.
 */
public class DeadlockDemo {
    private static final Object Lock1 = new Object();
    private static final Object Lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (Lock1) {
                System.out.println("Thread 1: Holding Lock 1...");
                try { 
                    // Simulate some processing to ensure t2 grabs Lock2
                    Thread.sleep(100); 
                } catch (InterruptedException e) { 
                    Thread.currentThread().interrupt(); 
                }
                System.out.println("Thread 1: Waiting for Lock 2...");
                synchronized (Lock2) {
                    System.out.println("Thread 1: Holding Lock 1 & 2");
                }
            }
        }, "Forge-Thread-1");

        Thread t2 = new Thread(() -> {
            synchronized (Lock2) {
                System.out.println("Thread 2: Holding Lock 2...");
                try { 
                    Thread.sleep(100); 
                } catch (InterruptedException e) { 
                    Thread.currentThread().interrupt(); 
                }
                System.out.println("Thread 2: Waiting for Lock 1...");
                synchronized (Lock1) {
                    System.out.println("Thread 2: Holding Lock 1 & 2");
                }
            }
        }, "Forge-Thread-2");

        t1.start();
        t2.start();
    }
}
Output
Thread 1: Holding Lock 1...
Thread 2: Holding Lock 2...
Thread 1: Waiting for Lock 2...
Thread 2: Waiting for Lock 1...
(Program hangs indefinitely)
Key Insight:
The most important thing to understand about Deadlock in Java — Causes and Prevention is the problem it was designed to solve. Always ask 'why does this exist?' before asking 'how do I use it?' In concurrency, we use locks to ensure data consistency, but deadlocks are the 'price' of incorrect lock ordering.
Production Insight
When you reproduce a deadlock in development, it often disappears with debugger breakpoints or Thread.sleep tweaks.
This happens because deadlocks are timing-sensitive — the window is narrow in low-contention environments.
Always stress-test with multiple threads spinning on the same resources to expose ordering issues.
Key Takeaway
Deadlocks require all four Coffman conditions.
Breaking any single condition prevents the deadlock.
Your job: ensure circular wait is impossible through global lock ordering.

The Four Coffman Conditions: A Deeper Look

Deadlock arises only when mutual exclusion, hold-and-wait, no preemption, and circular wait occur together. Understanding each condition helps you choose which to break.

Mutual exclusion: Resources are non-shareable (e.g., a file write lock). You can't avoid it when using locks. Hold-and-wait: A thread keeps locks while waiting for more. Break it by requiring all locks at once or using tryLock. No preemption: Locks are released only by the holder. Use ReentrantLock with timeout to simulate preemption. Circular wait: Impose a total order on all resources. This is the most practical to break in Java.

Let's examine strategies for each condition and their production trade-offs.

io/thecodeforge/concurrency/LockOrderingExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * io.thecodeforge: Lock ordering prevents circular wait.
 * Always acquire locks in ascending hash code order.
 */
public class LockOrderingExample {
    private static final Lock lockA = new ReentrantLock();
    private static final Lock lockB = new ReentrantLock();

    // System.identityHashCode to ensure consistent ordering even if lock objects differ
    private final int hashA = System.identityHashCode(lockA);
    private final int hashB = System.identityHashCode(lockB);

    public void safeMethod() {
        Lock firstLock = hashA < hashB ? lockA : lockB;
        Lock secondLock = hashA < hashB ? lockB : lockA;

        // Acquire in order
        firstLock.lock();
        try {
            secondLock.lock();
            try {
                System.out.println("Both locks acquired safely");
            } finally {
                secondLock.unlock();
            }
        } finally {
            firstLock.unlock();
        }
    }
}
Output
Both locks acquired safely (no deadlock)
Think of locking like putting on shoes and socks
  • Lock ordering is the socks-before-shoes rule for threads.
  • Circle wait breaks when you number all resources and always acquire from low to high.
  • Even if you forget the order, a tryLock with timeout acts like a safety net.
  • The mental model: 'If I'm holding resource 3, I must never ask for resource 2'.
Production Insight
Lock ordering based on System.identityHashCode works but can cause collisions if many locks.
Explicit ordering via a static integer ID per lock type is more predictable and easier to audit.
Don't rely on natural ordering of objects — it's fragile when new lock types are added.
Key Takeaway
Break circular wait by enforcing a global order on all lock acquisitions.
This is the single most effective prevention strategy.
Combine with tryLock for additional safety on high-contention paths.

The Dining Philosophers Problem: Classic Deadlock Illustration

The Dining Philosophers problem, introduced by Edsger Dijkstra in 1965, is the canonical example of deadlock in concurrent systems. Five philosophers sit at a round table with five chopsticks — one between each pair. Each philosopher alternates between thinking and eating. To eat, a philosopher must pick up both chopsticks (left and right). If all philosophers pick up their left chopstick simultaneously, none can pick up the right — circular wait forms, and the system deadlocks.

This problem maps directly to Java threads and locks. Each chopstick can be modeled as a Lock. Each philosopher is a thread that must acquire two locks (left and right chopstick). Without a lock ordering strategy, deadlock is inevitable. Solutions include: - Lock ordering: Assign a total order to chopsticks (e.g., by number) and always pick up the lower-numbered chopstick first. - tryLock: Use tryLock with timeout to release already held chopsticks if the second is unavailable. - Arbitrator: Introduce a waiter that limits the number of philosophers who can attempt to eat concurrently.

The problem teaches that even a simple round-robin resource sharing can deadlock without careful design.

io/thecodeforge/concurrency/DiningPhilosophersDeadlock.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * io.thecodeforge: Dining Philosophers — deadlock-prone implementation.
 */
public class DiningPhilosophersDeadlock {
    static class Philosopher extends Thread {
        private final Lock leftChopstick;
        private final Lock rightChopstick;
        private final String name;

        public Philosopher(Lock left, Lock right, String name) {
            this.leftChopstick = left;
            this.rightChopstick = right;
            this.name = name;
        }

        @Override
        public void run() {
            while (true) {
                // Think
                System.out.println(name + " is thinking...");
                try { Thread.sleep((long)(Math.random() * 100)); } catch (InterruptedException e) {}

                // Attempt to eat
                leftChopstick.lock();
                try {
                    System.out.println(name + " picked up left chopstick");
                    rightChopstick.lock();
                    try {
                        System.out.println(name + " is eating");
                        Thread.sleep((long)(Math.random() * 100));
                    } catch (InterruptedException e) {}
                    finally {
                        rightChopstick.unlock();
                    }
                } finally {
                    leftChopstick.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        Lock chopstick1 = new ReentrantLock();
        Lock chopstick2 = new ReentrantLock();
        Lock chopstick3 = new ReentrantLock();
        Lock chopstick4 = new ReentrantLock();
        Lock chopstick5 = new ReentrantLock();

        new Philosopher(chopstick1, chopstick2, "Philosopher1").start();
        new Philosopher(chopstick2, chopstick3, "Philosopher2").start();
        new Philosopher(chopstick3, chopstick4, "Philosopher3").start();
        new Philosopher(chopstick4, chopstick5, "Philosopher4").start();
        new Philosopher(chopstick5, chopstick1, "Philosopher5").start();
    }
}
Output
Philosopher1 picked up left chopstick
Philosopher2 picked up left chopstick
Philosopher3 picked up left chopstick
Philosopher4 picked up left chopstick
Philosopher5 picked up left chopstick
(All five are stuck waiting for right chopstick — deadlock)
Academic Insight:
The Dining Philosophers problem is the 'Hello World' of deadlock. It demonstrates that circular wait can arise even with symmetric, well-intentioned code. Solving it with lock ordering is the simplest fix, but tryLock with backoff is more robust in distributed systems.
Production Insight
This exact pattern appears in microservices that acquire two external resource locks (e.g., database rows or distributed leases). Always apply the lock ordering or tryLock pattern from this classic problem to real-world service coordination.
Key Takeaway
The Dining Philosophers problem is a concrete model of circular wait.
Use it as a mental template when designing any system with multiple resource locks.
Always break the cycle with ordering or timeouts.

Prevention Strategies: Lock Ordering and tryLock

Two proven techniques keep your code deadlock-free. Lock ordering is the foundation; tryLock adds a safety net.

Lock ordering involves defining a partial or total order for every lock in the system. For example, always acquire the database connection lock before the cache lock. This eliminates circular wait because threads can't hold a high-order lock while waiting for a lower-order one.

tryLock with timeout (available in ReentrantLock) allows a thread to back off if it can't acquire a lock within a certain time. This breaks the no-preemption condition — the thread effectively preempts itself. The downside: you must handle the failure case (rollback, retry, or degrade).

Both strategies together provide a robust defense. In production, the combination catches both design-time errors (lock ordering) and runtime contentions (tryLock backoff).

io/thecodeforge/concurrency/SafeLocking.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

/**
 * io.thecodeforge: Preventing Deadlock using tryLock with timeout.
 * This implementation breaks the 'Hold and Wait' condition.
 */
public class SafeLocking {
    private final Lock lockA = new ReentrantLock();
    private final Lock lockB = new ReentrantLock();

    public void performSafeTransfer() throws InterruptedException {
        boolean gotLockA = false;
        boolean gotLockB = false;

        try {
            // Attempt to acquire both locks within a specific timeframe
            gotLockA = lockA.tryLock(500, TimeUnit.MILLISECONDS);
            gotLockB = lockB.tryLock(500, TimeUnit.MILLISECONDS);

            if (gotLockA && gotLockB) {
                System.out.println("Resources acquired safely - Proceeding with Forge logic");
                // Perform critical section work here
            } else {
                System.out.println("Could not acquire locks safely. Backing off to prevent deadlock.");
            }
        } finally {
            if (gotLockA) lockA.unlock();
            if (gotLockB) lockB.unlock();
        }
    }
}
Output
// Resources acquired safely (or exits gracefully if timeout occurs)
Watch Out:
The most common mistake with Deadlock in Java — Causes and Prevention is using it when a simpler alternative would work better. Always consider whether the added complexity is justified. Often, using ConcurrentHashMap or Atomic variables removes the need for multiple manual locks entirely.
Production Insight
tryLock with a timeout adds latency: each failed attempt waits the full timeout before backing off.
Under high contention, this can multiply response times. Use small timeouts (e.g., 100 ms) and implement exponential backoff.
Consider a circuit breaker: if tryLock fails repeatedly, degrade the service (e.g., return cached data) instead of retrying forever.
Key Takeaway
Lock ordering prevents circular wait; tryLock prevents hold-and-wait.
Together they cover two conditions.
Always prefer lock ordering first — it costs nothing at runtime.

Detecting Deadlocks with Thread Dumps and JVM Tools

Deadlocks are invisible from the outside — no exceptions, no crashes. The only reliable way to detect them is by inspecting the state of all threads. The JVM's built-in deadlock detector in jstack and jcmd identifies cycles automatically.

Steps to diagnose: 1. Get the process ID: jps or ps aux | grep java 2. Run jstack <pid> and search for "Found one Java-level deadlock". 3. The output shows the cycle: which thread holds which lock, and what it's waiting for. 4. Also check for BLOCKED threads that are not part of a cycle but are waiting on a monitor held by a deadlocked thread.

For automated monitoring, use JDK's ThreadMXBean in your code to detect deadlocks programmatically and log them or trigger alerts.

io/thecodeforge/concurrency/DeadlockDetector.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.thecodeforge.concurrency;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

/**
 * io.thecodeforge: Programmatic deadlock detection using ThreadMXBean.
 */
public class DeadlockDetector {
    public static void checkForDeadlock() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            System.err.println("DEADLOCK DETECTED! Thread IDs: " + java.util.Arrays.toString(deadlockedThreads));
            // Log full thread dump for diagnosis
            StringBuilder dump = new StringBuilder();
            for (long tid : deadlockedThreads) {
                dump.append(threadMXBean.getThreadInfo(tid, Integer.MAX_VALUE).toString());
            }
            System.err.println(dump.toString());
            // Optionally trigger alert or health check failure
        }
    }
}
Output
// DEADLOCK DETECTED! Thread IDs: [123, 456]
Pro Tip:
Automate ThreadMXBean checks in a background scheduled task (e.g., every 30 seconds on a dedicated thread). When a deadlock is detected, write the dump to a log file and potentially restart the service automatically. At io.thecodeforge, we integrate this with our health check endpoints to fail the readiness probe until the deadlock is resolved.
Production Insight
Thread dumps are heavy — generating them under high load can pause the JVM briefly.
Don't run automated dump collection more frequently than every 10 seconds.
Use jcmd instead of jstack on production Java 8+ for lower overhead due to direct VM access.
Key Takeaway
Deadlock detection = jstack + ThreadMXBean.
jstack gives you the cycle; ThreadMXBean gives you automation.
Make deadlock detection part of your health check framework.

Beyond Deadlocks: Livelock, Starvation, and How to Distinguish Them

Not every concurrency hang is a deadlock. Livelock and starvation cause similar symptoms but require different fixes.

Livelock: Threads are active but make no progress — they keep retrying the same operation. Example: two threads use tryLock and upon failure, release their lock and retry, causing infinite mutual backoff.

Starvation: A thread is perpetually denied access to a resource because other threads continuously acquire it. Common with unfair locks or low-priority threads.

How to tell them apart
  • Deadlock: threads are BLOCKED (state BLOCKED).
  • Livelock: threads are RUNNABLE but CPU-bound doing no useful work.
  • Starvation: threads may be RUNNABLE or TIMED_WAITING but never get the lock.

Fix for livelock: add random jitter to retry intervals. Fix for starvation: use fair locks (new ReentrantLock(true)) or priority queues.

io/thecodeforge/concurrency/LivelockDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

/**
 * io.thecodeforge: Livelock simulation - threads keep retrying without backoff.
 */
public class LivelockDemo {
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (lock1.tryLock()) {
                    try { Thread.sleep(10); } catch (InterruptedException e) {}
                    if (lock2.tryLock()) {
                        // do work
                        lock2.unlock();
                        break;
                    } else {
                        lock1.unlock();
                    }
                }
            }
        });
        Thread t2 = new Thread(() -> {
            while (true) {
                if (lock2.tryLock()) {
                    try { Thread.sleep(10); } catch (InterruptedException e) {}
                    if (lock1.tryLock()) {
                        // do work
                        lock1.unlock();
                        break;
                    } else {
                        lock2.unlock();
                    }
                }
            }
        });
        t1.start(); t2.start();
    }
}
Output
// Both threads may loop forever without making progress
Key Distinction:
Livelock is often harder to detect than deadlock because threads appear active. Monitor CPU usage per thread: if all cores are near 100% but no throughput, suspect livelock.
Production Insight
In containerized environments, livelock can burn CPU credits on cloud instances, leading to throttling.
Always add a maximum retry count in tryLock loops — never retry indefinitely.
For fairness, consider an explicit backoff strategy: use Thread.sleep with random duration between retries.
Key Takeaway
Deadlock = blocked; Livelock = busy without progress; Starvation = denied access.
Each has different root causes and fixes.
Rule: add retry limits and random backoff to avoid livelock.

Deadlock vs Livelock vs Starvation: Comparison Table

Use the table below to quickly differentiate these three liveness failures when analyzing thread dumps or debugging production incidents.

FeatureDeadlockLivelockStarvation
Thread StateBLOCKEDRUNNABLERUNNABLE or TIMED_WAITING
CPU UsageLow (threads idle)High (threads busy waiting)Low to moderate
JVM DetectionAutomatic via jstack (finds cycle)Not detected — must monitor progressNot detected
Typical CauseCircular wait on locksMutual tryLock without backoffUnfair lock scheduling
Fix StrategyLock ordering, tryLockRandom backoff, retry limitFair locks, priority queues
Diagnostic Commandjstack find 'deadlock'top -Hp high CPU, jstack no deadlockjstack shows a thread never acquiring lock

Selecting the correct fix starts with correctly identifying which problem you face. Deadlock requires breaking the cycle; livelock needs backoff; starvation demands fairness tuning.

Production Alert:
Starvation can look like a slow down, not a stall. If one customer's request always times out while others succeed, check for a starved thread that never gets the lock. Use jstack and look for a thread that is waiting on a lock but never acquiring it.
Production Insight
In highly contended systems, starvation can be more insidious than deadlock because it degrades throughput without halting the service. Monitor per-thread completion times; if one thread is consistently slower than others, suspect starvation. For throughput-critical services, prefer fair ReentrantLock(true) or use thread pools with bounded queues to ensure no thread is indefinitely postponed.
Key Takeaway
Deadlock: threads blocked; Livelock: threads spinning; Starvation: threads denied.
JVM only detects deadlock automatically.
Diagnose using thread state and CPU usage patterns.

Advantages and Disadvantages of Understanding Deadlocks

Mastering deadlock theory and prevention has both benefits and drawbacks. This table summarizes the trade-offs every developer should consider.

AdvantagesDisadvantages
Prevents system freezes in high-concurrency applicationsRequires careful design and documentation of lock ordering
Reduces downtime and support escalationsAdds complexity with explicit Lock objects vs synchronized
Enables safe use of multiple resources per transactiontryLock with timeout increases code paths (failure handling)
Helps diagnose other liveness issues (livelock, starvation)Over-engineering locking can hurt performance if unnecessary
Foundation for distributed deadlock detection (e.g., database)Not all deadlocks are detectable by JVM (native locks)
Critical for real-time systems and financial transactionsRequires stress testing to validate — time-dependent

The key takeaway: invest in deadlock prevention early, but only where multiple locks are genuinely needed. In many cases, simpler concurrency utilities eliminate the risk entirely.

Practical Advice:
Don't apply deadlock prevention techniques to every piece of code. Use them only in paths that acquire two or more locks. For single-lock scenarios, deadlock is impossible. For lock-free data structures, you get concurrency without any of these pitfalls.
Production Insight
The biggest hidden cost of deadlock prevention is auditability. Lock ordering must be enforced across the team via code reviews and static analysis (e.g., ArchUnit rules). Without enforcement, one developer's well-intentioned commit can reintroduce circular wait. Budget time for documentation and automated checks.
Key Takeaway
Advantages: system stability, reduced incidents.
Disadvantages: design overhead, enforcement cost.
Apply prevention only where multiple locks are held concurrently.

Practice Problems to Master Deadlock Detection and Fixing

Sharpen your deadlock skills with these five real-world exercises. Each problem simulates a production scenario and tests your ability to reproduce, detect, and fix deadlocks.

Problem 1: Reproduce a Two-Lock Deadlock Write a Java program where two threads each acquire two locks in opposite order. Use synchronized blocks. Run it and confirm the program hangs. Use jstack to verify the circular wait.

Problem 2: Fix with Lock Ordering Take the program from Problem 1 and modify it so both threads acquire locks in the same order (e.g., lock1 before lock2). Verify the program completes without deadlock.

Problem 3: tryLock with Timeout Rewrite the deadlock program using ReentrantLock and tryLock with a 1-second timeout. If a thread cannot acquire both locks, it should release any held lock and retry up to 3 times. Log each attempt. Verify the program either succeeds or fails gracefully.

Problem 4: jstack Detective Simulate a deadlock in a Spring Boot application (or any long-running Java process). While the application is hung, take a thread dump using jcmd. Identify the deadlocked threads, the locks they hold, and the monitors they wait on. Write down the steps to diagnose.

Problem 5: Design a Deadlock-Free Ledger System You have three resources: Account, Transaction, and AuditLog. Thread A locks Account then Transaction. Thread B locks Transaction then AuditLog. Thread C locks AuditLog then Account. This creates a cycle across three threads. Design a lock ordering scheme that prevents deadlock. Implement a solution with a global lock ID assigned to each resource type.

io/thecodeforge/concurrency/LedgerSystemSafe.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * io.thecodeforge: Solution to Problem 5 — lock ordering across three resources.
 * Resource IDs: Account=1, Transaction=2, AuditLog=3.
 * Always acquire locks in ascending ID order.
 */
public class LedgerSystemSafe {
    private static final Lock accountLock = new ReentrantLock();
    private static final Lock transactionLock = new ReentrantLock();
    private static final Lock auditLogLock = new ReentrantLock();

    public static void doWork(int resourceId1, int resourceId2) {
        Lock first;
        Lock second;
        // Determine order based on IDs
        if (resourceId1 < resourceId2) {
            first = resourceId1 == 1 ? accountLock : resourceId1 == 2 ? transactionLock : auditLogLock;
            second = resourceId2 == 1 ? accountLock : resourceId2 == 2 ? transactionLock : auditLogLock;
        } else {
            first = resourceId2 == 1 ? accountLock : resourceId2 == 2 ? transactionLock : auditLogLock;
            second = resourceId1 == 1 ? accountLock : resourceId1 == 2 ? transactionLock : auditLogLock;
        }
        first.lock();
        try {
            second.lock();
            try {
                System.out.println("Both locks acquired in order, working safely.");
            } finally {
                second.unlock();
            }
        } finally {
            first.unlock();
        }
    }

    public static void main(String[] args) {
        // Thread A: Account (1) then Transaction (2)
        new Thread(() -> doWork(1, 2)).start();
        // Thread B: Transaction (2) then AuditLog (3)
        new Thread(() -> doWork(2, 3)).start();
        // Thread C: AuditLog (3) then Account (1) — safe because IDs 1 and 3, order 1 then 3
        new Thread(() -> doWork(3, 1)).start();
    }
}
Output
Both locks acquired in order, working safely.
Both locks acquired in order, working safely.
Both locks acquired in order, working safely.
Self-Study:
After solving these problems, try to reproduce the exact deadlock from the production incident described at the top of this article. Use jstack to confirm the cycle. Then apply the fix (lock ordering of Account before Ledger) and verify the program runs to completion.
Production Insight
These practice problems mirror real interview debugging exercises at Big Tech companies. The ability to quickly reproduce, diagnose, and fix a deadlock under time pressure is a strong signal of production readiness. Practice with a timer — aim to identify the cycle within 30 seconds of receiving a thread dump.
Key Takeaway
Reproduce → Fix → Detect → Design. These five problems cover the full deadlock skill spectrum. Master them to confidently handle production deadlocks.
● Production incidentPOST-MORTEMseverity: high

The Silent Freeze: Payment Service Goes Dark

Symptom
Gradual latency increase followed by complete stall. Thread pool queues fill, new requests hang. No OOM, no CPU spike.
Assumption
Engineers assumed locks were acquired in consistent order because the code looked similar. But different methods in different classes acquired locks in opposite order.
Root cause
Method A in PaymentService locked Account then Ledger. Method B in AuditService locked Ledger then Account. When both ran concurrently, circular wait formed.
Fix
Enforced a single lock ordering policy across the codebase: always lock Account before Ledger. Refactored AuditService to follow the same order. Also added tryLock with a timeout as a safety net in high-contention paths.
Key lesson
  • Lock ordering must be a documented, global convention — not left to per-class decisions.
  • Add thread dump automation: capture dumps automatically when latency thresholds are breached.
  • Test under high concurrency with a stress test that mirrors production load.
Production debug guideStep-by-step actions to identify, confirm, and resolve deadlocks in running JVMs4 entries
Symptom · 01
Application becomes unresponsive; health checks fail but no crash
Fix
Run jstack <pid> (or jcmd <pid> Thread.print) and look for 'deadlock' in the output. JVM automatically detects cycles.
Symptom · 02
Thread dumps show many BLOCKED threads waiting on the same monitor
Fix
Identify the monitor address (e.g., <0x000000076ae0b418>). Cross-reference with lock owners in other threads to confirm circular wait.
Symptom · 03
Deadlock only occurs under heavy load, not in development
Fix
Enable JVM flag -XX:+PrintConcurrentLocks to log all lock acquisitions. Replay load with a smaller thread pool to reproduce faster.
Symptom · 04
Suspected deadlock but jstack doesn't report one
Fix
Check for livelock or starvation: threads are not blocked but make no progress. Use jconsole or VisualVM to inspect thread states and CPU usage.
★ Deadlock Quick-Response CardWhen you suspect a deadlock in production, follow this sequence to minimize downtime.
Service unresponsive but process alive
Immediate action
Take thread dump immediately before restarting (kill -3 <pid> or jstack <pid> > dump.txt)
Commands
jstack <pid> | grep -A 10 'deadlock'
For a full dump, use: jcmd <pid> Thread.print > dump.txt
Fix now
Temporary fix: restart the service. Permanent fix: apply lock ordering and use tryLock with timeouts.
Stuck threads in Monitoring dashboards (e.g., Spring Boot Actuator /health hangs)+
Immediate action
Check thread pool health: thread pool queue size growing, active threads stuck. Actuator /threaddump returns no response.
Commands
curl -m 2 http://localhost:8080/actuator/threaddump || echo 'threaddump endpoint timed out'
ssh into node, then: top -Hp <pid> to see threads with high CPU or idle. Then jstack.
Fix now
Kill process (kill -9) and restart. Root cause analysis via dump afterwards.
Synchronized vs ReentrantLock for Deadlock Prevention
FeatureSynchronized BlocksReentrantLock API
Acquisition TypeImplicit (Block-based)Explicit (Method-based)
Non-blocking AttemptNo (Always blocks)Yes (tryLock())
Timeout SupportNoYes (tryLock(time, unit))
Fairness PolicyNo (Thread pick is random)Optional (Can favor long-waiting threads)
Interruption SupportNoYes (lockInterruptibly())
Deadlock RiskHigh (Circular Wait)Mitigated (via logic/timeouts)

Key takeaways

1
Deadlock in Java
Causes and Prevention is a core concept in Concurrency that every Java developer must master to ensure application 'liveness' and uptime.
2
The most effective prevention strategy is 'Lock Hierarchy'—always acquiring locks in a predefined, consistent order across the entire codebase.
3
Utilize explicit Lock objects from the java.util.concurrent.locks package when you need timeouts or interruptible lock acquisition.
4
Always release explicit locks in a finally block to ensure that resources are freed even if an unchecked exception occurs.
5
Monitor and profile
Use tools like JConsole or VisualVM to detect deadlocks during the development phase rather than waiting for a production freeze.

Common mistakes to avoid

5 patterns
×

Overusing manual locking when a thread-safe utility exists

Symptom
Code uses multiple synchronized blocks to manage a shared collection when ConcurrentHashMap or AtomicReference would suffice, increasing deadlock surface.
Fix
Replace manual locks with java.util.concurrent utilities. At io.thecodeforge, we prefer CopyOnWriteArrayList for read-heavy lists and ConcurrentHashMap for most maps.
×

Forgetting to release locks in exceptional cases

Symptom
When using Lock objects, an unchecked exception skips the unlock() call. The lock remains held forever, causing other threads to hang.
Fix
Always call unlock() in a finally block. For synchronized, the JVM releases the lock automatically — but for ReentrantLock and other explicit locks, you must ensure release. Use try-finally pattern.
×

Holding locks during I/O

Symptom
A thread acquires a lock and then performs a slow network call (e.g., HTTP request, database query). Other threads waiting for that lock are stuck for seconds or minutes.
Fix
Never call blocking I/O inside a synchronized block or while holding an explicit lock. Extract data first, release the lock, then perform I/O. Use asynchronous patterns if needed.
×

Deeply nested synchronized blocks with inconsistent ordering

Symptom
Method A locks resources in order (X then Y), method B locks (Y then X). When both run concurrently, circular wait forms.
Fix
Enforce a single global lock ordering across the entire codebase. Document the order (e.g., resource ID ascending). Use tools like Checkstyle or ArchUnit to enforce the rule.
×

Assuming 'synchronized' methods are immune to deadlock

Symptom
Two synchronized methods on different objects are called in sequence from two threads, causing a deadlock even without explicit nested locks.
Fix
Be aware that calling one synchronized method then another creates a nested lock scenario. Use lock ordering or combine the operations into a single synchronized block on a dedicated lock object.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is a Deadlock in Java and how does it differ from a Livelock or Sta...
Q02SENIOR
Explain the four Coffman conditions. Which one is the easiest to break i...
Q03SENIOR
How would you use a Thread Dump (jstack) to identify a deadlock in a run...
Q04SENIOR
What is the 'Lock Ordering' strategy and how does it prevent the Circula...
Q05SENIOR
Why is it better to use ReentrantLock's tryLock() method in a high-concu...
Q06SENIOR
Given two accounts, A and B, write a deadlock-free method to transfer mo...
Q01 of 06SENIOR

What is a Deadlock in Java and how does it differ from a Livelock or Starvation?

ANSWER
A deadlock occurs when two or more threads are stuck waiting for each other's locks, with no thread making progress. Livelock is similar but threads are active (e.g., looping) and not making useful progress. Starvation happens when a thread never gets a chance to run because other threads always acquire the resource first. Key diagnostic difference: deadlock → threads are in BLOCKED state; livelock → threads are RUNNABLE with no progress; starvation → threads may be TIMED_WAITING or BLOCKED but never acquire the monitor.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can deadlocks occur with synchronized blocks?
02
How does ThreadMXBean.findDeadlockedThreads() work?
03
What is 'lock contention' and how does it relate to deadlocks?
04
Should I use deadlock detection in production?
05
Is there a way to automatically break deadlocks in Java?
🔥

That's Concurrency. Mark it forged?

7 min read · try the examples if you haven't

Previous
Java CompletableFuture Explained
6 / 6 · Concurrency