What you'll learn
Quick Answer
Deadlock happens when two threads each hold a lock the other needs. It requires four conditions at once, and the simplest practical prevention is to make every thread acquire locks in the same global order.
The simplest possible deadlock
Two bank accounts, two transfers, at the same moment.
Thread 1 transfers from A to B. It locks A, then tries to lock B.
Thread 2 transfers from B to A. It locks B, then tries to lock A.
If both get their first lock before either gets its second, both wait forever. Thread 1 holds A and needs B; thread 2 holds B and needs A. Neither will release, because releasing happens after the transfer completes.
// Thread 1 // Thread 2
lock(accountA); lock(accountB);
lock(accountB); // waits lock(accountA); // waits
transfer(); transfer();
unlock(accountB); unlock(accountA);
unlock(accountA); unlock(accountB);
The unpleasant part is that this usually works. It only deadlocks when the timing interleaves exactly wrong, which might be once in ten thousand runs — passing every test and failing in production under load.
The four conditions
Deadlock requires all four simultaneously. This matters because breaking any one prevents it:
- Mutual exclusion — a resource can be held by only one thread at a time.
- Hold and wait — a thread holding one resource can request another.
- No preemption — a resource cannot be forcibly taken away; it must be released voluntarily.
- Circular wait — a cycle of threads each waiting for the next one's resource.
These are the Coffman conditions and they are frequently examined. The useful framing is that each suggests a prevention strategy, and in practice one of them is far easier to attack than the rest.
Break circular wait: lock ordering
The other three conditions are usually impractical to remove. Mutual exclusion is the point of a lock. Preemption means forcibly taking a lock mid-operation, which risks corrupt state. Removing hold-and-wait means acquiring everything at once, which is rigid and often impossible.
Circular wait is the practical target. If every thread acquires locks in the same global order, a cycle cannot form.
In the bank example, always lock the account with the lower ID first:
first = (a.id < b.id) ? a : b;
second = (a.id < b.id) ? b : a;
lock(first);
lock(second);
transfer();
unlock(second);
unlock(first);
Now both threads lock the lower ID first. One gets it, the other waits for that single lock, and the first completes. No cycle, no deadlock, and the change is small enough to apply consistently.
The other common technique is a lock timeout: try to acquire, and if it does not succeed within a set time, release everything and retry. That converts a permanent hang into a recoverable delay, at the cost of possible repeated retries.
Recognising it when it happens
The signature is distinctive once you know it: the program stops responding, CPU usage is near zero, and nothing is logged. Compare with an infinite loop, which pins a core at 100%. Zero CPU while stuck means waiting, not working.
In Java, take a thread dump — it explicitly reports detected deadlocks along with which threads hold and want which locks. In Python, faulthandler can dump stacks of all threads. Databases detect deadlocks between transactions automatically and abort one with a deadlock error, which is why you sometimes see that in application logs.
That database behaviour is worth knowing: deadlock is not only a threading concern. Two transactions updating the same rows in different orders deadlock the same way, and the fix is the same — consistent ordering.
Related problems that are not deadlock
- Livelock — threads are active and responding to each other but make no progress. Two people stepping aside repeatedly in a corridor. CPU is busy, work is not done.
- Starvation — a thread never gets the resource because others keep taking priority. Not stuck, just never scheduled.
- Race condition — the opposite failure. Not too much locking but too little, so shared state gets corrupted. See process vs thread for a measured example.
Race conditions and deadlocks pull in opposite directions, which is the central tension of concurrent programming: too little locking corrupts data, too much stops progress. The way out is to hold as few locks as possible, for as short a time as possible, always in the same order.
