Definition
A deadlock occurs when two or more threads each hold a lock the other needs and neither will ever release, so all of them wait forever. You diagnose it from a thread dump, where the deadlocked threads appear in the BLOCKED state, each "waiting to lock" a monitor that another thread "holds." Modern JVMs even print an explicit "Found one Java-level deadlock" section. You fix it with consistent global lock ordering, shorter critical sections, or lock acquisition with a timeout using tryLock.
Understanding the Concepts
What causes a deadlock?
A deadlock needs four conditions to hold at once: mutual exclusion (locks are exclusive), hold-and-wait (a thread holds one lock while waiting for another), no preemption (locks cannot be forcibly taken away), and circular wait (a cycle of threads each waiting on the next). Break any one condition and deadlock becomes impossible. The most practical condition to break is circular wait, which you eliminate by always acquiring locks in a consistent global order.
How does a deadlock appear in banking code?
The classic banking example is two simultaneous transfers in opposite directions.
package com.bank.service;
import java.math.BigDecimal;
public class UnsafeTransferService {
// DEADLOCK RISK: locks accounts in parameter order
public void transfer(Account from, Account to, BigDecimal amount) {
synchronized (from) { // Thread 1 locks Rajesh's account
synchronized (to) { // Thread 1 waits for Priya's account
from.debit(amount);
to.credit(amount);
}
}
}
}
// Thread 1: transfer(rajesh, priya, 5000) → locks rajesh, waits for priya
// Thread 2: transfer(priya, rajesh, 3000) → locks priya, waits for rajesh
// DEADLOCK: both threads wait forever
How do you diagnose it?
To diagnose a deadlock, you take a thread dump with jstack or by sending the JVM a quit signal. In the dump, deadlocked threads are in the BLOCKED state. The HotSpot JVM detects this automatically.
jstack <pid>
# Output includes:
# Found one Java-level deadlock:
# =============================
# "transfer-thread-1":
# waiting to lock monitor 0x00007f... (Priya's Account)
# which is held by "transfer-thread-2"
# "transfer-thread-2":
# waiting to lock monitor 0x00007f... (Rajesh's Account)
# which is held by "transfer-thread-1"
How do you fix it?
The fixes follow from breaking circular wait.
Fix 1: Consistent lock ordering – always acquire locks in the same global order, for example by account ID.
package com.bank.service;
import java.math.BigDecimal;
public class SafeTransferService {
public void transfer(Account from, Account to, BigDecimal amount) {
// Always lock the account with the lower ID first
Account first = from.getId().compareTo(to.getId()) < 0 ? from : to;
Account second = first == from ? to : from;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}
}
Fix 2: tryLock with timeout – use ReentrantLock and back off if the second lock is not available.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public void transferWithTryLock(Account from, Account to, BigDecimal amount)
throws InterruptedException {
while (true) {
if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
from.debit(amount);
to.credit(amount);
return; // success
} finally { to.getLock().unlock(); }
}
} finally { from.getLock().unlock(); }
}
Thread.sleep(50); // back off and retry
}
}
Fix 3: Shorter critical sections – hold each lock for as little code as possible. Never call unknown external code while holding a lock.
How do you detect deadlocks programmatically?
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedIds = bean.findDeadlockedThreads();
if (deadlockedIds != null) {
System.out.println("DEADLOCK DETECTED! Threads: " + deadlockedIds.length);
// Log and alert
}
This is great for a health check endpoint that logs and alerts if any deadlock ever forms in production.
What should a senior engineer know?
- Distinguish deadlock from livelock and contention. Deadlock: threads BLOCKED forever, no progress. Livelock: threads active but making no progress (keep retrying and failing). Contention: threads wait but eventually proceed.
- Only true deadlock never resolves on its own. Livelock may resolve with randomized backoff. Contention resolves when the lock holder finishes.
- In banking at National Bank of India, Rajesh added the deadlock detector as a periodic health check. When two transfers in opposite directions caused a deadlock, the thread dump immediately showed the cycle, and the fix was consistent ordering by account ID.
Key Takeaway
A deadlock happens when two or more threads each hold a lock that another needs and none of them will release, so they all wait forever and make no progress. It needs four conditions at once – mutual exclusion, hold-and-wait, no preemption, and a circular wait – and breaking any single one makes deadlock impossible. The most practical one to break is the circular wait. To diagnose a deadlock I take a thread dump with jstack or by sending the process a quit signal, and the deadlocked threads show up in the BLOCKED state, each one "waiting to lock" a monitor that another thread "holds." The HotSpot JVM is helpful here because it prints an explicit "Found one Java-level deadlock" section that names the threads and the locks, so I usually do not even have to trace the cycle by hand.
The fixes all come from breaking that circular wait. The cleanest is consistent lock ordering: I define a stable global order for locks, for example by account number, and always acquire them in that order so a cycle can never form. I also keep critical sections as short as possible and never call out to unknown code while holding a lock. When ordering is impractical, I use a ReentrantLock with tryLock and a timeout, so a thread that cannot get the second lock in time backs off, releases what it holds, and retries, which breaks the hold-and-wait condition. And for safety I add a runtime check using ThreadMXBean's findDeadlockedThreads, so if a deadlock ever forms in production it is detected, logged, and alerted instead of silently hanging.
In my banking work at National Bank of India, this was a real incident. Our transfer service hung with no errors and no CPU usage whenever two transfers ran at the same time in opposite directions, like Rajesh to Priya and Priya to Rajesh, because each thread locked its source account first and then blocked on the destination. The thread dump showed both threads BLOCKED in exactly that cycle. We fixed it by always locking the account with the lower account id first, which guarantees both transfers acquire the locks in the same order and the cycle can never happen. I also added the deadlock detector as a health check so we would catch any future ordering mistake immediately rather than discovering it from a frozen service.
Remember one sentence: How to diagnose thread deadlock: thread dump shows threads in BLOCKED state waiting for each other's locks: fix by consistent lock ordering, shorter critical sections, or using tryLock with timeout is fundamental knowledge for Java interviews and production banking work.
---