Deadlock, Livelock, and Starvation

A thread does not need to crash to fail. It can also simply stop making progress and sit there forever, consuming a slot in the thread pool or holding a connection open, while your monitoring shows no exceptions at all. There are three distinct ways this happens, and they call for different fixes.

Deadlock: two threads, two locks, opposite order

Deadlock happens when two or more threads each hold a lock the other one needs, and each refuses to give up the lock it already has:

public class DeadlockDemo {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (LOCK_A) {
                sleep(50);
                synchronized (LOCK_B) {
                    System.out.println("t1 acquired both locks");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (LOCK_B) {
                sleep(50);
                synchronized (LOCK_A) {
                    System.out.println("t2 acquired both locks");
                }
            }
        });

        t1.start();
        t2.start();
    }

    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException ignored) {
        }
    }
}

t1 grabs LOCK_A and then wants LOCK_B; t2 grabs LOCK_B and then wants LOCK_A. Neither line ever prints. Both threads sit in the BLOCKED state forever, each waiting for a lock the other one will never release. A thread dump at this point shows exactly this pattern: each thread's stack ends in a wait for a lock owned by another thread that is itself waiting. The reliable fix is always the same idea: establish a single, consistent global ordering for acquiring locks, so that no two threads can ever want them in opposite order.

Livelock: busy, active, and going nowhere

Livelock looks different from the outside — CPU usage is high, threads are clearly running — but no real progress happens, because the threads keep politely reacting to each other in a way that undoes their own progress. A common cause is a thread that, on failing to grab a second lock, backs off and retries in a way that stays perfectly in step with the other thread doing the same thing:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockDemo {
    static class Resource {
        private final Lock lock = new ReentrantLock();

        void useWith(Resource other, String who) {
            while (true) {
                if (lock.tryLock()) {
                    try {
                        if (other.lock.tryLock()) {
                            try {
                                System.out.println(who + " acquired both resources");
                                return;
                            } finally {
                                other.lock.unlock();
                            }
                        }
                    } finally {
                        lock.unlock();
                    }
                }
                // politely back off and retry - the trap
                try {
                    Thread.sleep(10);
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

    public static void main(String[] args) {
        Resource r1 = new Resource();
        Resource r2 = new Resource();

        new Thread(() -> r1.useWith(r2, "A")).start();
        new Thread(() -> r2.useWith(r1, "B")).start();
    }
}

Each thread grabs its own resource first, then tries the other one, fails because the other thread is holding it, releases its own resource, waits, and tries again. If both threads happen to retry on the same rhythm, they can keep colliding indefinitely: both busy, both making tryLock calls, neither ever holding both locks at once. Unlike deadlock, the threads here are not stuck waiting — they are stuck retrying. The usual fix is to break the symmetry, for example by backing off for a random amount of time instead of a fixed one, so the two threads eventually fall out of step.

Starvation: always losing the race for a turn

Starvation is when a thread is technically able to run, but some other thread or threads consistently gets scheduled first, so the starved thread makes little or no progress for a very long time. It can come from thread priorities set too aggressively, from a lock implementation that tends to favor whichever thread most recently held it, or simply from a small number of greedy threads that never yield. Unlike deadlock, starvation is not a hard guarantee of never finishing — it is a statistical failure, which makes it harder to reproduce and diagnose, since the starved thread eventually does get a turn, just far later than acceptable.

Telling them apart in practice

All three produce the same outward symptom: something that should have finished has not. The distinguishing signal is CPU usage and thread state. Deadlocked threads show as BLOCKED with zero CPU usage and a thread dump revealing a cycle of lock ownership. Livelocked threads show as RUNNABLE, burning CPU, with no forward progress across repeated dumps taken a few seconds apart. Starved threads are intermittently RUNNABLE, making slow but nonzero progress, typically alongside other threads consuming a disproportionate share of CPU time. Reading a thread dump with this distinction in mind turns "the app is stuck" into a specific, fixable diagnosis.

Share