Picture a busy four-way intersection at rush hour where the traffic lights have failed. A car edges into the box heading north, blocking the car that wants to go east. That eastbound car, now sitting in the box, blocks the one going south. The southbound car blocks the westbound one — and the westbound car blocks our original northbound driver. Every car is waiting for the car in front to move, and the car in front is you, four hops around the circle. Horns blare, nobody has crashed, nobody is breaking a rule — and yet not a single car can advance. Gridlock.
That is a deadlock: a ring of threads in which each one holds a resource the next one
needs, and every one of them is frozen, waiting for a neighbour that will never let go. It is the dark
side of the very
This page is about the three ways concurrent progress can stall even when no single thread has a bug:
deadlock (a frozen cycle), livelock (threads busily doing nothing),
and starvation (one thread perpetually shoved to the back of the queue). They are
cousins, they are easy to confuse, and telling them apart is half the battle. (The operating-systems
module treats deadlock detection and the banker's algorithm in depth under
You do not need a philosophers' banquet to deadlock. Two threads and two locks will do it. Both
threads need both locks. Thread A takes m1 then reaches for m2;
thread B takes m2 then reaches for m1. Read the timeline down the middle:
Notice how little it takes. No crash, no corrupted data, no obviously wrong line of code — just two perfectly correct threads that happened to grab the same two locks in the opposite order at the wrong instant. The interleaving is a matter of luck, which is what makes deadlocks so vicious: the program runs fine ten thousand times and wedges on the ten-thousand-and-first, on a customer's machine, at 3 a.m.
In 1971 Edward Coffman and colleagues pinned down precisely what a deadlock requires. The elegant part is that all four conditions must hold at the same time — knock out any single one and deadlock becomes structurally impossible. That single fact is the lever behind every prevention strategy.
A deadlock can occur only if all four of these hold simultaneously:
Read them back against the gridlocked intersection: the box holds one car at a time (mutual exclusion); each car sits in its spot while wanting the next spot (hold and wait); no car can be lifted out by a crane (no preemption); and each waits on the next, all the way around (circular wait). Remove any one — a traffic cop who waves a car back (preemption), or a rule that you may not enter the box unless your exit is clear (no hold-and-wait) — and the jam dissolves. The magic number is not "fix all four"; it is "break any one."
The clearest way to spot a deadlock is to draw it. In a resource-allocation graph we use two shapes and two kinds of arrow:
Step through the graph below. Thread
The four arrows form a directed cycle
Because all four Coffman conditions are needed at once, prevention just means making one of them structurally impossible. Three practical techniques a concurrency programmer actually reaches for, each attacking a different pillar:
m1 before m2, so it waits at the very first step while
holding nothing.
tryLock(). If it fails, release the
first lock too, and retry from scratch. Now no thread ever sits holding one lock while blocked on
another.
Lock ordering is the one you should reach for first: it is cheap, static, and provably kills the cycle. The other two trade a guarantee for flexibility — and, as we'll see, back-off has a trap of its own.
The canonical deadlock story is Dijkstra's Dining Philosophers. Five philosophers sit around a table; between each pair is a single fork. To eat, a philosopher needs both neighbouring forks. If every philosopher picks up their left fork at once, every fork is held, every philosopher waits for their right fork — a five-way circular wait. Classic deadlock.
Two famous fixes map straight onto the pillars above. The resource hierarchy solution numbers the forks and makes everyone pick up the lower-numbered fork first — that is lock ordering, and it breaks the cycle because at least one philosopher (the one straddling the highest and lowest fork) reaches for them in the opposite direction. The waiter solution introduces an arbitrator who only lets a philosopher pick up forks if both are free — that negates hold and wait. Same dinner, two different pillars knocked out.
Our sandbox runs on a single thread, so it can't really hang — which is perfect, because it
means we can script the exact interleaving that deadlocks and detect it by inspecting who-holds-what,
rather than freezing the page. The program below plays the bad schedule (A takes m1, B
takes m2, then each reaches for the other's lock) and reports the frozen state. Then it
re-runs with a global lock ordering — both threads always take m1 before
m2 — and shows it completes. Press Run ▶:
Same two locks, same two threads — only the order in which they acquire changed. In attempt 1
the opposite orders form a cycle; in attempt 2 the shared order means whoever loses the race for
m1 simply waits at the first step, holding nothing, so there is nothing to deadlock on.
Deadlock's sneaky sibling looks the opposite from the outside and yet achieves the same nothing. Picture two people meeting in a narrow corridor. You step left to let them pass; they step to their right — the same side — so you are face to face again. You both apologise, step the other way, and once more you mirror each other. You are both moving, both being polite, both responding — and neither of you gets past. That is a livelock.
In code, livelock is the trap in the try-lock-and-back-off scheme above. Two threads each grab their first lock, fail to get the second, politely release and retry — and if they retry in lockstep, they collide again on the next round, and the next, forever. Nobody is blocked. The threads are running flat out, the CPU is pegged at 100%, and progress is exactly zero.
The cure is to break the symmetry that keeps the two threads in perfect step. Add randomised back-off: after failing, each thread waits a random little while before retrying. Now the two retries almost never line up, one thread slips through first, and the dance ends. (Ethernet's collision handling and countless network retry loops use exactly this "exponential back-off with jitter" idea for the same reason.)
It is tempting to diagnose "the program is stuck, must be a deadlock." But a deadlocked program is
quiet: its threads are asleep, CPU usage near zero, and a debugger shows them all parked
inside lock(). A livelocked program is loud: CPU pinned at
100%, threads racing through the same retry loop over and over, making zero forward progress. Same
symptom to the user ("it hangs"), opposite fingerprint under the hood. If your "hung" program is
burning a whole core, suspect livelock — and reach for jitter, not for lock ordering.
The third failure is subtler still, because the system as a whole is making progress — just not for one unlucky thread. Starvation is when a thread is perpetually denied a resource it needs, while other threads keep sailing through. It isn't frozen and it isn't spinning uselessly; it is simply, endlessly, passed over.
The usual culprit is unfair scheduling. Imagine a lock that, whenever it is released, is handed to the highest-priority waiter. A steady stream of high-priority threads arrives, and a lone low-priority thread that has been waiting since the start never gets its turn — there is always someone "more important" ahead of it. Or a reader/writer lock that always favours readers: as long as one reader is always reading, a writer waiting to update can wait indefinitely.
The fix is fairness: guarantee bounded waiting so that once a thread asks for
a resource, only a limited number of others can jump ahead of it before it is served. A first-come,
first-served (FIFO) queue on the lock, or a fair lock that hands ownership to the longest waiter, turns
"might wait forever" into "waits at most
A close relative of starvation is priority inversion. A low-priority thread grabs a lock; a high-priority thread then needs the same lock and blocks — so the "important" thread is now waiting on the "unimportant" one. Worse, if medium-priority threads keep preempting the low-priority holder, it never runs, never releases the lock, and the high-priority thread is stuck behind it indefinitely.
This famously struck NASA's 1997 Mars Pathfinder: a high-priority bus-management task kept missing its deadline because a low-priority task held a shared lock while being starved by medium-priority work, and the watchdog timer rebooted the spacecraft — repeatedly, millions of miles away. The fix, applied by radio uplink, was priority inheritance: while a high-priority thread waits on a lock, the holder temporarily inherits that high priority so it can finish and release quickly. A tidy patch to a very expensive lesson.
It's worth holding all three side by side, because the everyday symptom — "it's hung" — is the same, while the cause and the cure differ completely:
| Failure | Are the threads running? | Who makes progress? | Typical cure |
|---|---|---|---|
| Deadlock | No — blocked on locks | Nobody in the cycle | Break a Coffman condition (e.g. lock ordering) |
| Livelock | Yes — spinning at 100% CPU | Nobody — they keep reacting | Break the symmetry (randomised back-off / jitter) |
| Starvation | Yes — the system runs | Some do; one is perpetually denied | Fairness / bounded waiting (FIFO, fair locks) |
Deadlock is a property of a whole set of threads at once; livelock and starvation are about a thread (or two) failing to advance while, in starvation's case, the world moves on without them. Knowing which one you're looking at tells you which tool to grab.