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.

| Condition | Meaning | How we remove it |
|---|---|---|
| Mutual exclusion | Only one thread can hold the lock | Use lock-free classes such as AtomicInteger or ConcurrentHashMap |
| Hold and wait | A thread holds one lock while it waits for another | Take one lock at a time, or all locks at once with tryLock() |
| No preemption | Nobody can take a lock away from its holder | Use tryLock(timeout) or lockInterruptibly() |
| Circular wait | Threads wait for each other in a cycle | Take 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
- ThreadMXBean Javadoc (Java 25)
- Lock Javadoc (Java 25)
- The jcmd Command (Java 25)
- Oracle Java Tutorials, Deadlock
Happy Learning !!
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.
Hi Lokesh,
Great Explanation!
Can we achieve dead lock using Class Level Locking instead of using object level locking?
Great Explain :) Simple but valuable
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.
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.
Greate job, thanks
Basic Factors for Deadlock
1) Mutual Exclusion
2) Hold and Wait
3) No Preemption
4) Circular Wait
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.
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!
Nothing special. I was just being lazy.
Nice example. Thanks for sharing.
hi LOKESH,
can i do the deadlock by using synchronized methods(not synchronized blocks).
Hi Lokesh,
i m getting same output in both case please do need fully.
It’s not the output that matters, it’s deadlock concept in discussion. Output may/or may not differ, both are perfectly fine.
Loved your approach of getting to the root problem by first understanding the pattern. Youd rocked it!