Course topics

By WebNest Studio Engineering Team

Java - Core Tutorial

Deadlock

A deadlock occurs when two or more threads are each waiting for a resource (usually a lock) held by another thread in the same cycle, so none of them can ever proceed. Once a deadlock happens, the affected threads are stuck permanently — the JVM does not automatically detect or break deadlocks involving intrinsic locks.

Why it matters

A deadlock does not throw an exception or print an error. The affected threads simply stop, forever, usually under load and often only in production. Knowing the conditions that make it possible lets you design it out — the simplest rule, always taking locks in the same order, prevents most cases.

The Four Necessary Conditions

Classic operating-systems theory identifies four conditions that must all hold simultaneously for a deadlock to occur. If any one of them is broken, deadlock becomes impossible.

  • Mutual exclusion — a resource (lock) can be held by only one thread at a time.
  • Hold and wait — a thread holds at least one resource while waiting to acquire another.
  • No preemption — a resource cannot be forcibly taken from a thread; it must be released voluntarily.
  • Circular wait — a cycle of threads exists where each is waiting for a resource held by the next thread in the cycle.

A Concrete Two-Lock Deadlock

The most common real-world deadlock shape happens when two threads each need two locks, but acquire them in opposite order: Thread A locks Resource 1 then tries to lock Resource 2, while Thread B locks Resource 2 then tries to lock Resource 1. If both threads acquire their first lock at nearly the same time, each ends up waiting forever for a lock the other one holds.

Prevention Strategies

Deadlock is prevented by breaking one of the four necessary conditions, and in practice the most common techniques are consistent lock ordering and using timed lock attempts instead of indefinite blocking.

  • Lock ordering — always acquire multiple locks in the same global order across every thread, which eliminates circular wait.
  • Lock timeouts — use java.util.concurrent.locks.Lock.tryLock(timeout, unit) instead of synchronized so a thread can back off and retry rather than block forever.
  • Minimizing lock scope — hold locks for the shortest time possible and avoid acquiring a second lock while already holding one, whenever the logic allows it.
  • Using higher-level concurrency utilities — classes in java.util.concurrent (like ConcurrentHashMap or BlockingQueue) avoid manual multi-lock coordination entirely.

Example

A reproducible two-lock deadlock (this program will hang, illustrating the problem)

Java
public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread-1 locked A, waiting for B");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockB) {
                    System.out.println("Thread-1 acquired both locks");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread-2 locked B, waiting for A");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockA) {
                    System.out.println("Thread-2 acquired both locks");
                }
            }
        });

        t1.start();
        t2.start();
        // Both threads print their first line, then block forever waiting on
        // each other's lock. "acquired both locks" never prints for either thread.
    }
}
Output
Thread-1 locked A, waiting for B
Thread-2 locked B, waiting for A
(The program then hangs indefinitely — a genuine deadlock. Fixing it means having both threads
acquire lockA before lockB, i.e. a consistent global lock order.)

Common Mistakes

  • Acquiring multiple locks in inconsistent order across different threads or methods — the single most common real cause of deadlocks.
  • Holding a lock while calling into unrelated code (a callback, a listener) that might itself try to acquire another lock, creating a hidden circular dependency.
  • Assuming the JVM will detect and recover from a deadlock automatically — for intrinsic (synchronized) locks it does not; the threads hang forever unless the process is killed.
  • Using synchronized everywhere "to be safe" without thinking about lock acquisition order between methods that each take multiple locks.

Exercise

These two threads take the same two locks in opposite orders, so the program can hang. Change the second thread so that both threads acquire the locks in the same order. The program then always finishes.

Expected output: done

Java · starter code
public class LockOrder {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread first = new Thread(() -> {
            synchronized (lockA) {
                synchronized (lockB) {
                    // work using both locks
                }
            }
        });

        Thread second = new Thread(() -> {
            synchronized (lockB) {
                synchronized (lockA) {
                    // work using both locks
                }
            }
        });

        first.start();
        second.start();
        first.join();
        second.join();
        System.out.println("done");
    }
}
Show hints
  • A deadlock needs a cycle: thread one holds A and wants B, thread two holds B and wants A.
  • If every thread takes lockA before lockB, no cycle can form.
Show solution
Java · solution
public class LockOrder {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread first = new Thread(() -> {
            synchronized (lockA) {
                synchronized (lockB) {
                    // work using both locks
                }
            }
        });

        Thread second = new Thread(() -> {
            synchronized (lockA) {
                synchronized (lockB) {
                    // work using both locks
                }
            }
        });

        first.start();
        second.start();
        first.join();
        second.join();
        System.out.println("done");
    }
}

Quick quiz

1. Which of these is NOT one of the four conditions required for a deadlock?

  • A. Mutual exclusion
  • B. Hold and wait
  • C. Circular wait
  • D. Time slicing
Show answer

D. The four conditions are mutual exclusion, hold and wait, no preemption and circular wait. Time slicing is a scheduling technique.

2. Which practice prevents most deadlocks?

  • A. Always acquiring locks in the same order
  • B. Using more threads
  • C. Raising thread priority
  • D. Calling Thread.sleep() between locks
Show answer

A. A consistent global lock order removes the possibility of a circular wait.

3. How can you confirm that a hung Java application is deadlocked?

  • A. Restart it
  • B. Take a thread dump, for example with jstack
  • C. Increase the heap size
  • D. Check the console for an exception
Show answer

B. A thread dump lists each thread's state and the locks it holds and waits for, and reports Java-level deadlocks explicitly.

Interview questions

What is a deadlock and what conditions cause it?

Show answer

A deadlock is a state in which two or more threads wait forever, each holding a lock the other needs. It requires four conditions together: mutual exclusion (a lock is held by one thread at a time), hold and wait (a thread keeps one lock while waiting for another), no preemption (a lock cannot be taken away), and circular wait (the threads form a cycle). Breaking any one of them prevents deadlock.

What is the difference between deadlock, livelock and starvation?

Show answer

In a deadlock, threads are blocked and make no progress. In a livelock, threads are active but keep reacting to each other and still make no progress, like two people repeatedly stepping aside for one another. In starvation, one thread never gets the lock or processor time it needs because others keep taking it, while the rest of the system carries on.

Key Points to Remember

  • Deadlock requires all four conditions at once: mutual exclusion, hold-and-wait, no preemption, and circular wait.
  • A classic deadlock arises when two threads acquire two shared locks in opposite order.
  • Consistent lock ordering across the whole codebase is the most reliable prevention strategy.
  • tryLock() with a timeout (from java.util.concurrent.locks) allows a thread to back off instead of blocking forever.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering