Deadlock in Java: Create, Detect and Resolve It

Deadlock in Java explained with a deterministic example, jstack and ThreadMXBean detection, lock ordering, tryLock timeouts and interruptible recovery.

Deadlock cycle where thread checkout holds the inventory lock and waits for wallet, while thread refund holds wallet and waits for inventory

A deadlock in Java happens when two or more threads each hold a lock and wait for a lock that another of them holds, so none of them can ever continue. The threads stay in the BLOCKED or WAITING state forever, the JVM does not throw an exception, and the requests they serve hang.

We need to know how deadlocks form so we can prevent them in code that takes more than one lock, such as a money transfer between two accounts, and so we can detect them in a running app with a thread dump or with ThreadMXBean.

The following example shows the pattern behind most deadlocks, two threads that take the same two locks in opposite order.

Object inventory = new Object();
Object wallet = new Object();
Runnable checkout = () -> { synchronized (inventory) { synchronized (wallet) { } } };   // inventory, then wallet
Runnable refund = () -> { synchronized (wallet) { synchronized (inventory) { } } };     // wallet, then inventory

Notice the opposite order of the locks. When checkout gets inventory and refund gets wallet at the same moment, each waits for the other forever. We create such a deadlock on purpose, detect it with a thread dump and in code, and fix it with lock ordering, timeouts and interruptible locks.

1. What Causes a Deadlock in Java

A deadlock needs four conditions at the same time, known as the Coffman conditions. Removing any one of them makes a deadlock impossible, which is why each fix later in the article targets one condition.

Deadlock cycle where thread checkout holds the inventory lock and waits for wallet, while thread refund holds wallet and waits for inventory
Each thread holds one lock and waits for the other, so the wait-for graph forms a cycle
ConditionMeaningHow we remove it
Mutual exclusionOnly one thread can hold the lockUse lock-free classes such as AtomicInteger or ConcurrentHashMap
Hold and waitA thread holds one lock while it waits for anotherTake one lock at a time, or all locks at once with tryLock()
No preemptionNobody can take a lock away from its holderUse tryLock(timeout) or lockInterruptibly()
Circular waitThreads wait for each other in a cycleTake locks in one global order

Say a banking app moves money between two accounts and locks both accounts for each transfer, the source first. Alice sends 10 dollars to Bob while Bob sends 5 dollars to Alice. The first transfer locks Alice’s account, the second locks Bob’s, and both wait for the other account forever, together with every later request for these two accounts.

2. Creating a Deadlock That Always Happens

Many deadlock demos call the sleep method between the two locks and hope that the other thread gets its first lock in the meantime. A CountDownLatch makes it certain. Each thread takes its first lock, counts down and waits until the other thread holds its first lock too. After that, it asks for its second lock.

static Thread lockBoth(String name, Object first, Object second, CountDownLatch bothHoldFirst) {
    Thread thread = new Thread(() -> {
        synchronized (first) {
            bothHoldFirst.countDown();
            try {
                bothHoldFirst.await();                  // until the other thread holds its first lock
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            synchronized (second) {                     // never succeeds
                System.out.println(name + " finished");
            }
        }
    }, name);
    thread.setDaemon(true);                             // lets the demo JVM exit
    thread.start();
    return thread;
}
Object inventory = new Object();
Object wallet = new Object();
CountDownLatch bothHoldFirst = new CountDownLatch(2);
Thread checkout = lockBoth("checkout", inventory, wallet, bothHoldFirst);
Thread refund = lockBoth("refund", wallet, inventory, bothHoldFirst);
checkout.join(500);                                     // gives up waiting after 500 ms
Thread.State state = checkout.getState();               // BLOCKED

Both threads end in the BLOCKED state of the thread life cycle, and nothing will ever wake them. We made them daemon threads only so the demo program can exit. In a server, the two threads would stay stuck until a restart.

3. Detecting a Deadlock

A deadlock rarely shows up in tests, because it needs a specific timing. In production, it looks like requests that never finish while the CPU is idle. Two tools tell us which threads are stuck and which locks they hold.

3.1. Reading a Thread Dump

A thread dump lists every thread with its stack and its locks, and the JVM adds a deadlock report at the end. We find the process id with jps and take the dump with jstack or jcmd, both included in the JDK.

jps -l
jstack 12345
jcmd 12345 Thread.print

The report names each thread, the lock it waits for and the thread that holds that lock. The following output comes from jcmd on JDK 25 for the program in section 2, trimmed to the deadlock section. Line 14 of DeadlockDemo.java is the inner synchronized (second) block.

Found one Java-level deadlock:
=============================
"checkout":
  waiting to lock monitor 0x00007f3d14078b40 (object 0x00000000a851beb0, a java.lang.Object),
  which is held by "refund"

"refund":
  waiting to lock monitor 0x00007f3d20001450 (object 0x00000000a851bea0, a java.lang.Object),
  which is held by "checkout"

Java stack information for the threads listed above:
===================================================
"checkout":
	at DeadlockDemo.lambda$lockBoth$0(DeadlockDemo.java:14)
	- waiting to lock <0x00000000a851beb0> (a java.lang.Object)
	- locked <0x00000000a851bea0> (a java.lang.Object)
"refund":
	at DeadlockDemo.lambda$lockBoth$0(DeadlockDemo.java:14)
	- waiting to lock <0x00000000a851bea0> (a java.lang.Object)
	- locked <0x00000000a851beb0> (a java.lang.Object)

Found 1 deadlock.

The stack lines point to the source lines that take the locks, which is where the fix goes. Named threads make the report much easier to read than Thread-0 and Thread-1. The thread dump guide covers the other ways to take a dump, and the jstack post shows the command on Linux. JConsole and JDK Mission Control have a Detect Deadlock button that runs the same check.

3.2. Finding Deadlocked Threads in Code

The ThreadMXBean interface runs the same check from inside the app. Its method findDeadlockedThreads() returns the ids of threads that are deadlocked on monitors or on java.util.concurrent locks, and null when there are none. The older findMonitorDeadlockedThreads() sees only synchronized monitors.

ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] ids = mxBean.findDeadlockedThreads();
int deadlocked = ids == null ? 0 : ids.length;          // 2
List<String> names = Arrays.stream(mxBean.getThreadInfo(ids)).map(ThreadInfo::getThreadName).sorted().toList();   // [checkout, refund]

A health check or a scheduled watchdog can call this method every minute, log the ThreadInfo of the stuck threads and mark the instance as unhealthy, so the platform restarts it. The check is not free, because it inspects every thread, so we do not call it in a tight loop.

4. Fixing a Deadlock With Lock Ordering

If every thread takes its locks in the same global order, a cycle cannot form, so the deadlock cannot happen. Swapping the order in one thread fixes a single demo, but it breaks again as soon as somebody adds a third code path. A rule based on data works for every caller.

For the banking app, we lock the account with the lower id first, no matter which account is the source of the transfer.

class Account {
    final int id;
    int balance;

    Account(int id, int balance) {
        this.id = id;
        this.balance = balance;
    }
}
static void transfer(Account from, Account to, int amount) {
    Account first = from.id < to.id ? from : to;        // always lock the lower id first
    Account second = first == from ? to : from;
    synchronized (first) {
        synchronized (second) {
            from.balance -= amount;
            to.balance += amount;
        }
    }
}
Account alice = new Account(1, 100);
Account bob = new Account(2, 100);
Thread toBob = Thread.ofPlatform().start(() -> { for (int i = 0; i < 10_000; i++) transfer(alice, bob, 1); });
Thread toAlice = Thread.ofPlatform().start(() -> { for (int i = 0; i < 10_000; i++) transfer(bob, alice, 1); });
toBob.join();
toAlice.join();
int total = alice.balance + bob.balance;                // 200
int aliceBalance = alice.balance;                       // 100

Both threads finish, and no money is lost. When objects have no natural id, we can order them by a unique key such as a UUID. We do not order by System.identityHashCode() alone, because two objects can share a hash code.

5. Avoiding a Deadlock With tryLock and a Timeout

When a global order is hard to keep, for example when the second lock is only known after the first one is taken, we use a ReentrantLock with tryLock(timeout, unit). A thread that cannot get the second lock in time releases the first one, waits a random short time and tries again, which removes the hold-and-wait condition.

static boolean transferWithTimeout(ReentrantLock fromLock, ReentrantLock toLock, Runnable move) throws InterruptedException {
    for (int attempt = 0; attempt < 3; attempt++) {
        if (fromLock.tryLock(50, TimeUnit.MILLISECONDS)) {
            try {
                if (toLock.tryLock(50, TimeUnit.MILLISECONDS)) {
                    try {
                        move.run();
                        return true;
                    } finally {
                        toLock.unlock();
                    }
                }
            } finally {
                fromLock.unlock();                      // released before the next attempt
            }
        }
        long backOff = ThreadLocalRandom.current().nextLong(10, 50);
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(backOff));
    }
    return false;
}

In the following example, another thread holds Bob’s lock for longer than three attempts take, so the transfer gives up and reports false instead of hanging. Once Bob’s lock is free, the same call succeeds.

ReentrantLock aliceLock = new ReentrantLock();
ReentrantLock bobLock = new ReentrantLock();
CountDownLatch bobLocked = new CountDownLatch(1);
CountDownLatch releaseBob = new CountDownLatch(1);
Thread other = Thread.ofPlatform().start(() -> {
    bobLock.lock();
    try {
        bobLocked.countDown();
        releaseBob.await();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        bobLock.unlock();
    }
});
bobLocked.await();
boolean moved = transferWithTimeout(aliceLock, bobLock, () -> {});        // false, Bob's lock stayed busy
boolean aliceFree = !aliceLock.isLocked();                              // true, released after each attempt
releaseBob.countDown();
other.join();
boolean movedLater = transferWithTimeout(aliceLock, bobLock, () -> {});   // true

A timeout does not prove a deadlock, because the other thread may only be slow. The caller has to handle false, for example by returning HTTP 503 or queuing the transfer for a retry. The random back-off keeps two threads from retrying in lockstep, which would turn the deadlock into a livelock.

6. Breaking a Deadlock at Runtime

A thread that waits to enter a synchronized block cannot be interrupted, so a synchronized deadlock lasts until the JVM stops. A thread that waits in lockInterruptibly() of a ReentrantLock throws InterruptedException when another thread interrupts it, releases its locks in finally and lets the other thread continue.

The following example builds the same deadlock with interruptible locks, detects it with ThreadMXBean and interrupts one of the two threads. The worker() method records how each thread ended.

static Thread worker(String name, ReentrantLock first, ReentrantLock second, CountDownLatch bothHoldFirst, Map<String, String> results) {
    return Thread.ofPlatform().name(name).start(() -> {
        try {
            first.lockInterruptibly();
            try {
                bothHoldFirst.countDown();
                bothHoldFirst.await();
                second.lockInterruptibly();             // the deadlock forms here
                try {
                    results.put(name, "completed");
                } finally {
                    second.unlock();
                }
            } finally {
                first.unlock();
            }
        } catch (InterruptedException e) {
            results.put(name, "interrupted, locks released");
        }
    });
}
ReentrantLock stock = new ReentrantLock();
ReentrantLock payment = new ReentrantLock();
CountDownLatch bothHoldFirst = new CountDownLatch(2);
Map<String, String> results = new ConcurrentHashMap<>();
Thread order = worker("order", stock, payment, bothHoldFirst, results);
Thread cancel = worker("cancel", payment, stock, bothHoldFirst, results);
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
boolean cancelStuck = false;
while (!cancelStuck) {                                  // wait until the cycle forms
    long[] ids = mxBean.findDeadlockedThreads();
    cancelStuck = ids != null && Arrays.stream(ids).anyMatch(id -> id == cancel.threadId());
    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
}
cancel.interrupt();                                     // break the cycle
order.join();
cancel.join();
String orderResult = results.get("order");              // "completed"
String cancelResult = results.get("cancel");            // "interrupted, locks released"

The cancel thread gives up and releases the payment lock, so the order thread gets it and completes. In a real app, the interrupted request fails with an error and can be retried, which is better than two requests that hang forever. The InterruptedException guide shows how to pass the interrupt on correctly. Interrupting is a recovery step, not a fix, so we still correct the lock order in the code that the thread dump points to.

7. Java Deadlock FAQs

Questions about deadlocks also cover two related failures, livelock and starvation, and what the JVM does when threads get stuck.

7.1. What is the difference between deadlock, livelock and starvation?

In a deadlock, threads wait for each other and do nothing. In a livelock, threads keep running and reacting to each other, for example by releasing and retrying a lock in the same rhythm, but none makes progress. In starvation, one thread never gets a lock because other threads always get it first, which a fair lock such as new ReentrantLock(true) prevents.

7.2. Does the JVM resolve deadlocks on its own?

No. The JVM reports deadlocks in thread dumps and through ThreadMXBean, but it never interrupts or kills a thread to break one. The stuck threads stay blocked until the app breaks the cycle with interruptible locks or the process restarts.

7.3. Can a single thread deadlock?

Yes, with a lock that is not reentrant. A thread that holds the only permit of a binary semaphore and calls acquire() again waits for itself forever, and so does a thread that calls Thread.currentThread().join(). A synchronized block or a ReentrantLock cannot deadlock this way, because both are reentrant.

7.4. Can virtual threads deadlock?

Yes. Virtual threads use the same synchronized monitors and java.util.concurrent locks, so two virtual threads that take two locks in opposite order deadlock like platform threads. Lock ordering and tryLock() prevent it the same way. Detection is harder, because ThreadMXBean supports only platform threads, so findDeadlockedThreads() does not report deadlocked virtual threads.

8. Conclusion

A deadlock forms when threads hold locks and wait for each other in a cycle. We find it with a thread dump from jstack or jcmd, or in code with ThreadMXBean.findDeadlockedThreads(), and the report names the threads, the locks and the source lines involved.

The fix is to take locks in one global order, such as by account id. Where that is not possible, tryLock() with a timeout and a random back-off avoids the wait, and lockInterruptibly() lets a watchdog break a deadlock that has already formed, which synchronized cannot do.

9. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Great explanation but 100% deadlock here is still NOT guaranteed as in case where thread-1 completes its run() calling before thread-0 initiates its call to its run(). Its all depend on CPU scheduler.

  2. Hi Lokesh,

    Great Explanation!

    Can we achieve dead lock using Class Level Locking instead of using object level locking?

  3. Hi,
    I have a project about make deadlock and solve it.But i do not know about what should i do for my projects.
    please help me.

  4. Hey Lokesh,
    What if the resources that are locked are passed as parameters by threads , that is a & b are passed as parameters and one thread passes x & y to a, b and the other passes y & x. Can you talk about that scenario if possible. Thanks.

    • In such scenario, you can use any type of ordering on a and b e.g. Object.identityHashCode() can be used to compare any type of object and based on that comparison you can decide the lock order.

  5. Basic Factors for Deadlock

    1) Mutual Exclusion
    2) Hold and Wait
    3) No Preemption
    4) Circular Wait

  6. Thanks for this article.
    I had to design “Resource Manager” application for a multi-user system. This Resource Manager should monitor the requests made by the users (or processes) on resources of the system.
    i got some basic idea through this article.

  7. Hello!
    Why have you made resourses as internal classes? I tryed to create two Integer-reference objects and they do the same work perfectly. In my opinion, example without these internal classes looks clearer. Do you have some special meaning for this solution?
    In any case, thank you for good explanations!

  8. Loved your approach of getting to the root problem by first understanding the pattern. Youd rocked it!

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.