JAVA / THREADS AND CONCURRENCY
Deadlocks, livelocks and diagnosing stuck programs
Diagnose a hung Java program by telling deadlock from livelock, detect lock cycles with ThreadMXBean and thread dumps, and remove them by ordering locks.
What you will learn
- Spot a lock-order cycle: two threads each holding one lock and wanting the other
- Detect a lock cycle at runtime with ThreadMXBean.findDeadlockedThreads()
- Tell deadlock (BLOCKED, frozen) from livelock (RUNNABLE, spinning) in a dump
- Break cycles with one global lock order or tryLock with a timeout fallback
Understanding Deadlocks, livelocks and diagnosing stuck programs
A deadlock is not a broken lock, it is a cycle in the graph of who waits for whom. In the example below one worker holds LEFT and asks for RIGHT while the other holds RIGHT and asks for LEFT, so each thread waits for the only thing the other could release. Entering a synchronized block has no timeout and ignores interrupts, so once a thread is BLOCKED inside that cycle it stays there until the process dies. The cycle is created purely by the order in which the two locks are taken, which is why every real fix changes the order, the duration, or the number of locks held at once.
Livelock is the opposite mechanism with the same symptom. Each thread does notice the conflict and does back out, but both back out and retry on the same rhythm, so they collide again forever: the threads stay RUNNABLE, the CPU stays busy, the stacks change every time you look, and still no unit of work finishes. The cure is to break the symmetry, normally with randomized backoff or a rule that decides which participant keeps its lock, not with a shorter retry delay.
Diagnosis comes down to comparing two thread dumps taken a few seconds apart. jcmd <pid> Thread.print prints every stack and adds a section headed Found one Java-level deadlock when the JVM sees a cycle of monitors or ReentrantLocks, and ThreadMXBean.findDeadlockedThreads answers the same question from inside the process. If no cycle is reported, the thread states decide: identical stacks in state WAITING point at a signal that never arrived, such as a wait with no matching notify or a get on a Future that no pool thread will ever run, while stacks that keep moving with no completed work point at livelock.
Fixing a stuck program means removing the possibility of the cycle rather than making it rarer.
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
public class StuckProgram {
private static final Object LEFT = new Object();
private static final Object RIGHT = new Object();
public static void main(String[] args) throws InterruptedException {
startWorker("left-then-right", LEFT, RIGHT);
startWorker("right-then-left", RIGHT, LEFT);
Thread.sleep(1500); // give both workers time to get stuck
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
long[] ids = mx.findDeadlockedThreads();
if (ids == null) {
System.out.println("no lock cycle found");
return;
}
ThreadInfo[] infos = mx.getThreadInfo(ids);
String[] lines = new String[infos.length];
for (int i = 0; i < infos.length; i++) {
lines[i] = infos[i].getThreadName() + " is " + infos[i].getThreadState()
+ ", waiting for a lock held by " + infos[i].getLockOwnerName();
}
Arrays.sort(lines); // the JVM does not define the discovery order
System.out.println("deadlocked threads: " + ids.length);
for (String line : lines) {
System.out.println(" " + line);
}
}
private static void startWorker(String name, Object first, Object second) {
Thread t = new Thread(() -> {
synchronized (first) {
sleep(300); // hold the first lock long enough to collide
synchronized (second) {
System.out.println(name + " got both locks");
}
}
}, name);
t.setDaemon(true); // stuck daemon threads do not keep the JVM alive
t.start();
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Deadlock is a cycle in the waits-for graph and livelock is a symmetric retry loop, and the thread states in a dump tell you which one you are looking at.
Worked examples
Livelock: busy and going nowhere
Two threads that politely release their first lock whenever the second is taken keep retrying in lockstep and complete nothing.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class LivelockDemo {
static final ReentrantLock A = new ReentrantLock();
static final ReentrantLock B = new ReentrantLock();
static final CyclicBarrier GATE = new CyclicBarrier(2);
static final AtomicInteger attempts = new AtomicInteger();
static final AtomicInteger transfers = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> retryPolitely(A, B), "t1");
Thread t2 = new Thread(() -> retryPolitely(B, A), "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("attempts: " + attempts.get());
System.out.println("transfers: " + transfers.get());
}
static void retryPolitely(ReentrantLock first, ReentrantLock second) {
for (int round = 0; round < 5; round++) { // a real livelock has no cap
attempts.incrementAndGet();
first.lock();
try {
sync(); // both threads hold their first lock at this point
if (second.tryLock()) {
try {
transfers.incrementAndGet();
} finally {
second.unlock();
}
return;
}
} finally {
first.unlock(); // back out and try the whole thing again
}
}
}
static void sync() {
try {
GATE.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new IllegalStateException(e);
}
}
}Example explained
Line 1GATE.await() runs while each thread already holds its first lock, so by the time either calls tryLock the other lock is certainly taken.
Line 2second.tryLock() returning false instead of parking is exactly what makes this a livelock and not a deadlock.
Line 3first.unlock() in the finally block is the polite retreat that keeps both threads runnable and keeps both failing.
Line 4attempts 10 with transfers 0 is the signature: two attempts per round, five rounds, no completed work; the cap of 5 only exists so the program ends.
Removing the cycle with a global lock order
Sorting the two monitors by account id before acquiring them makes a cycle impossible, so both transfer directions can run concurrently.
public class OrderedLocking {
static 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 low = from.id < to.id ? from : to;
Account high = (low == from) ? to : from;
synchronized (low) { // always the smaller id first
synchronized (high) {
from.balance -= amount;
to.balance += amount;
}
}
}
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1, 1000);
Account b = new Account(2, 1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) transfer(a, b, 1);
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) transfer(b, a, 1);
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a=" + a.balance + " b=" + b.balance);
System.out.println("total=" + (a.balance + b.balance));
}
}Example explained
Line 1low and high are derived from the ids, so transfer(a, b, 1) and transfer(b, a, 1) both lock account 1 before account 2.
Line 2Written naively as synchronized (from) then synchronized (to), these two threads would form a cycle within a few iterations and hang.
Line 3Both balances change while both monitors are held, so no thread can see a half-finished transfer.
Line 4join() before printing gives main a happens-before edge to both workers, so total is exactly 2000 on every run.
Important notes
findDeadlockedThreads only sees cycles through owned locks, meaning monitors and ReentrantLock-style synchronizers; a hang caused by exhausted Semaphore permits or a CountDownLatch is invisible to it because those have no owning thread.
Interrupting a thread that is BLOCKED entering a synchronized block has no effect; only ReentrantLock with lockInterruptibly or tryLock lets a thread abandon the attempt.
Common mistakes
Assuming synchronized methods on different objects cannot deadlock: a.moveTo(b) and b.moveTo(a) running at once still nest two monitors in opposite orders, and the program freezes with no exception and nothing in the log.
Responding to a livelock by shortening the retry delay, which makes the two threads collide more often and burn more CPU; only randomized or asymmetric backoff removes the lockstep.
Expecting the JVM to report every hang: an unmatched wait, or Future.get on a task no pool thread will run, leaves threads WAITING forever while findDeadlockedThreads returns null and the dump contains no deadlock section.
Try it yourself
Change, predict, then run
Change the second call in the main example to startWorker("also-left-then-right", LEFT, RIGHT) and rerun it. Confirm that both workers now print that they got both locks and the report says no lock cycle found, because a single acquisition order cannot form a cycle.
Open the Java workspaceCheck your understanding
Two thread dumps taken ten seconds apart show the same two threads in state RUNNABLE inside a retry loop, CPU is near 100 percent, and jcmd reports no Java-level deadlock. What is the most likely explanation?
- A deadlock on two monitors that the JVM failed to detect
- A missed notify, so both threads are waiting for a signal that never arrives
- Livelock: each thread takes one lock, fails to get the second, releases and retries in step with the other
- A visibility bug that would disappear if the shared flag were declared volatile
Show answer
RUNNABLE threads with churning stacks and high CPU are still executing code, which is livelock rather than deadlock. A monitor deadlock would show both threads BLOCKED with identical frozen stacks and the JVM reports those cycles reliably, so the undetected-deadlock option does not fit; a missed notify would show state WAITING with almost no CPU use.