A deadlock is when two or more threads each hold something the other needs, so neither can move again and the program stops making progress without crashing. The fix is structural: take locks in one consistent order, keep the set of held locks small, and never hold a lock across a slow call. That is the short version, and the rest of this guide is the detail behind each of those moves.
Deadlocks are awkward because nothing throws. The process stays alive, the log stays quiet, and the symptom is that throughput drops to zero while every thread reports itself as “running.” If you have ever watched a service take four minutes to answer a request that normally takes forty milliseconds, this is the failure you were probably staring at.
Below is what deadlock detection looks like in practice, how to pull the proof out of a live process, and the specific changes that remove it in Java, Python, C++, and SQL. I have kept the theory to what actually helps when you are debugging at two in the morning.
Table of Contents
- What Is a Deadlock?
- What Causes a Deadlock?
- The two-process trace everyone should recognise
- How Do You Know a Program Has a Deadlock?
- How to Prevent a Deadlock
- Deadlock Prevention Strategies in Detail
- How to Recover When a Deadlock Has Occurred
- Deadlocks in Operating Systems, Databases, and Applications
- Frequently Asked Questions
- Can timeouts completely prevent deadlocks?
- What is the difference between deadlock, livelock, and starvation?
- Does using a single lock always prevent deadlocks?
- How do I find a deadlock in a multithreaded program?
- Are database transaction deadlocks different from thread deadlocks?
- What is the Banker’s algorithm?
- Conclusion
What Is a Deadlock?

A deadlock is a state where a set of processes or threads are each blocked, waiting for a resource or lock held by another member of the same set, so that none of them can ever proceed. Each one holds what it needs to keep going and wants something it cannot have until the first one releases, and nothing in the system will ever break that tie on its own.
The word comes from ordinary life rather than computing. Two cars nose-first in a narrow lane, a door that cannot be opened from the inside because the other side is locked the same way, a jury split evenly and unable to reach a verdict. All of them share the shape: two parties, each waiting on the other, and no third party coming to help.
What separates a deadlock from ordinary waiting is that the wait is permanent without outside intervention. A thread blocked on a socket read is waiting, and the read will finish when data arrives. A deadlocked thread is blocked on a resource that will never be released, because the thread that would release it is blocked on something else in the same cycle.
Starvation is the near miss. A starved thread also waits forever, but the system keeps making progress around it, because other threads keep winning the race for the contested resource. In a deadlock the whole set is stuck; in starvation the rest of the system is busy.
What Causes a Deadlock?
Four conditions have to hold at the same moment. Edward Coffman and colleagues set them out in 1971, and every practical deadlock you will meet is one of these four breaking, or all four holding.
| Condition | In plain English | How you break it |
|---|---|---|
| Mutual exclusion | The resource can only be held by one holder at a time | Use a lock-free structure when the critical section is tiny, or allow multiple readers |
| Hold and wait | A thread holds one resource while asking for a second | Require all locks up front and release them together, or take them one at a time and release before waiting |
| No preemption | You cannot forcibly take a resource back from a thread that holds it | Design resources to be stealable or restartable, which is hard and often not worth it |
| Circular wait | There is a closed loop of waits, A waits on B waits on C waits on A | Impose a global lock order so a cycle cannot form |
Circular wait is the one that actually gets attacked in practice, because it is the only condition you can control cheaply in application code. Breaking it means every lock in the system gets a rank, and threads only ever acquire locks in ascending rank order.
The two-process trace everyone should recognise
Two threads, two mutexes. This is the shape of almost every real deadlock, whether the resources are locks, file handles, database rows, or connection pool slots.
// Java - this deadlocks
synchronized (lockA) {
synchronized (lockB) { // Thread 1 holds A, wants B
transfer();
}
}
synchronized (lockB) {
synchronized (lockA) { // Thread 2 holds B, wants A
transfer();
}
}
- Thread 1 takes lockA. It now holds one lock and wants lockB.
- Thread 2 takes lockB. It now holds one lock and wants lockA.
- Thread 1 blocks on lockB, which Thread 2 holds.
- Thread 2 blocks on lockA, which Thread 1 holds.
- Neither thread can reach the closing brace, so neither lock is ever released.
Both threads are in state BLOCKED or WAITING, both are perfectly healthy by every health check you have, and neither will ever move again.
The everyday triggers look nothing like textbook mutexes, which is why they survive code review. Nested locks held across a network call or a disk read are the single most common cause, and they are invisible in a diff because the dangerous line is the synchronized block several frames away. Two transactions updating the same pair of tables in opposite order is the database version, and it fires under production concurrency and never in staging with one user.
File locking produces the same shape when two processes open the same pair of files for writing in opposite order. The thread-pool case is sneakier: workers that submit tasks to the same pool and then block on their futures deadlock the moment the queue fills, so it looks fine until load climbs.
How Do You Know a Program Has a Deadlock?
Start with the signature. Progress stops, CPU for the affected threads drops to zero, no exception is logged, and the same requests that were fine a minute ago now queue forever. If CPU is spinning at 100 percent on every core, you are looking at livelock or a busy loop, not a deadlock.
The confirmation is a stack dump of the stuck threads. Take three of them, about ten seconds apart, and compare. Identical stacks mean blocked threads stay blocked, which is what a deadlock looks like from the outside.
# Java: three dumps, ten seconds apart
jstack <pid> > dump1.txt
sleep 10
jstack <pid> > dump2.txt
diff dump1.txt dump2.txt
# Python, without pausing the process
py-spy dump --pid <pid>
In a Java dump, look for the Found one Java-level deadlock section that the JVM prints when it detects a cycle itself. When it does not print, read the - locked <0x...> (a java.util.concurrent.locks.ReentrantLock) lines on each blocked thread and build the graph by hand. Two threads whose locked-address sets overlap are pointing at each other.
In a Python dump the equivalent evidence is threads parked inside acquire() rather than running Python frames. Follow each thread’s target lock back to the thread that owns it, and the cycle appears the same way.
Databases tell you directly. SQL Server exposes sys.dm_os_waiting_tasks joined to sys.dm_exec_requests, MySQL prints the two transactions and the statements that collided in SHOW ENGINE INNODB STATUS, and PostgreSQL raises deadlock_detected (SQLSTATE 40P01) with a detail line naming both process IDs. The names are the graph, already drawn for you.
For a hung Windows service, WinDbg gives you the same thing with !locks and the kernel lock list, and a native stack with ~*k. For native processes on Linux, gdb -p <pid> and thread apply all bt is enough to confirm every thread is parked in a futex wait.
Profilers are useful once you know what to look for. A wall-clock flame graph where every thread is stacked inside the same lock() call is the signature. Async-profiler in wall-clock mode, py-spy top, and strace showing a process with nothing but futex calls all point the same direction.
How to Prevent a Deadlock

The five-item prevention checklist, in the order I would work through it: impose a consistent global lock order, take all the locks you need at once instead of one at a time, keep critical sections short and free of I/O, put a timeout on every lock acquisition, and drop the lock entirely when the work under it is small enough to do with an atomic.
Each of those is a trade, not a free win. Here is what each one costs and when it is worth paying.
Deadlock Prevention Strategies in Detail
1. Acquire locks in one consistent global order. Rank every mutex in the system. A thread that needs three locks takes them lowest rank first, always. No cycle can form because a wait edge can only ever point from a higher rank to a lower one, and a loop needs to climb back to where it started.
This is the fix I reach for first. It costs a little coordination between components that would otherwise not need to know about each other, which is the real price, not the runtime overhead.
// C++ - std::scoped_lock takes both at once and backs off on contention,
// so the "hold and wait" condition never applies.
std::mutex accountA, accountB;
void transfer() {
std::scoped_lock guard(accountA, accountB); // no ordering needed
// move the money
}
2. Request everything at once, or nothing. The hold-and-wait condition disappears if a thread never holds one lock while waiting for another. All-or-nothing acquisition is the cleanest expression of that.
// Java - tryLock with a timeout turns an infinite wait into a failure you handle
if (lockB.tryLock(200, TimeUnit.MILLISECONDS)) {
try {
if (lockA.tryLock(200, TimeUnit.MILLISECONDS)) {
try { transfer(); }
finally { lockA.unlock(); }
}
} finally { lockB.unlock(); }
}
3. Keep critical sections short and keep I/O out of them. Every extra line inside a lock is another thread’s extra wait, and any call that can block is a chance to form a cycle with another subsystem. Move the file write, the HTTP call, and the logging outside the lock by computing first, locking second, acting third.
This is the one with the best ratio of effort to payoff. Nothing clever, no coordination across components, and it shortens every wait in the system.
# Python - the same shape, with a timeout so a stuck acquire surfaces
result = compute_under_no_lock(payload)
with cache.lock:
cache.store(key, result)
with file_lock.acquire(timeout=5):
write_report(result)
4. Put a timeout on every acquisition. A timeout does not remove the deadlock, it converts it from a hang into an exception you can retry. That is a genuine improvement in production, but it is a mitigation, not a fix, and it needs to be paired with a backoff or it becomes a retry storm.
-- SQL Server: the NOWAIT hint returns an error instead of waiting
BEGIN TRAN;
SELECT * FROM accounts WITH (UPDLOCK, ROWLOCK, NOWAIT)
WHERE id = 42;
COMMIT TRAN;
Choose the timeout from your own latency budget. Too low and you fail healthy traffic under load, too high and you have rebuilt the hang with extra steps.
5. Remove the lock when the work is small enough. If the critical section is a counter, a flag, or a single field update, use an atomic operation and there is no lock to deadlock on. For database rows, optimistic concurrency with a version column does the same job, and the failure mode is a retryable conflict instead of a blocked transaction.
The trade-off is real: optimistic schemes burn work on conflicts, so they lose to pessimistic locks when contention is heavy and every write touches the same rows.
Two more that get skipped. Taking every lock a code path needs in one std::lock or scoped_lock call, and keeping a documented lock hierarchy in the repository so new code inherits the order instead of inventing one.
How to Recover When a Deadlock Has Occurred
You cannot unfreeze a live deadlock by argument. Recovery is always some form of taking a participant out and putting it back.
The bluntest option is process termination, which is what the JVM does and what most databases do to a victim transaction. Kill one member of the cycle and the rest drain as their locks are released. It is safe only because a deadlock means no partial work can have escaped yet, which is true of locks and not true of anything else.
Transaction rollback is the database equivalent. The engine picks a victim, rolls it back, and returns error 1213 on SQL Server or SQLSTATE 40P01 on PostgreSQL. The application is expected to retry the transaction, and that expectation is the whole design: the error is normal traffic, not an exception path.
Retry with jitter, then. A bare retry loop where every thread wakes at the same instant and re-collides is how a deadlock turns into a livelock: the system burns full CPU, retries forever, and completes nothing. Exponential backoff with a random component breaks the synchrony, and a cap on attempts turns sustained failure into a circuit breaker instead of an outage.
What not to do is swallow the exception and move on. Catching a timeout, logging it, and returning an empty result hides the cycle and leaves the system quietly degraded.
Deadlocks in Operating Systems, Databases, and Applications
The same four conditions show up in four different places, and the useful mental model changes with the layer.
| Layer | What the lock protects | How a deadlock surfaces | Main prevention lever |
|---|---|---|---|
| Kernel and OS | Kernel data structures, files, device drivers, the page cache | Uninterruptible processes in D state, a hung mount, a driver waiting on user space | Fixed lock ordering in the kernel, and never calling into user space while holding a lock |
| User-space threads | Application data structures | Application stops progressing with no error | Consistent lock order, short critical sections, timeouts |
| Database | Rows, ranges, indexes, gap locks | Vendor error with a deadlock code and both participants named | Consistent access order across transactions, short transactions, NOWAIT or a deadlock timeout setting |
| Distributed services | Distributed transactions, replica locks, message ordering | Partial progress, timeouts, or a stuck consumer | Short transactions, idempotent retries, and timeouts, because there is no shared memory to order locks in |
That last row deserves the extra note. Across machines you cannot take a global lock order, and a global coordinator would cost more than the problem. What you use instead is the deadlock_timeout setting (PostgreSQL defaults to one second) plus retries, and the distributed-systems literature calls the result a phantom deadlock: a delay or partition that looks like a cycle but is really a message that never arrived.
On the strategy question that comes up constantly in operating-systems courses: prevention removes a condition so the deadlock can never form, avoidance checks whether a request would move the system into an unsafe state and waits if it would, detection lets deadlocks happen and finds them afterwards, and the Ostrich algorithm simply ignores them and hopes. Prevention is cheap in overhead and hard in discipline; avoidance needs advance knowledge of every process’s maximum claim, which is why the Banker’s algorithm appears in textbooks and rarely in services.
The Banker’s algorithm in one paragraph, since the name comes up constantly: it keeps a vector of currently available resources and a matrix of what each process has and what it could ever need, then repeatedly grants a request only if some process could still finish with its current allocation plus what is free. If that finishes every process in turn, the state is safe and the grant proceeds. If not, the state is unsafe and the request waits. The cost is knowing every maximum claim in advance, which you never know in a real system.
Frequently Asked Questions
Can timeouts completely prevent deadlocks?
No. A timeout converts an infinite block into an exception your code can catch, so the process stays alive and the failure becomes visible, but the underlying cycle is still there and the next call may form a new one. Treat timeouts as a safety net over a real fix such as a consistent lock order, and always pair them with backoff so retries do not turn a deadlock into a livelock.
What is the difference between deadlock, livelock, and starvation?
A deadlock is a closed cycle of waits where every participant is blocked and nothing progresses. Livelock is different: threads keep changing state and reacting to each other, so CPU stays high but no work completes, like two cars edging forward and backing off forever. Starvation is when one thread never wins the resource while the system as a whole keeps running normally. Only deadlock leaves everything stopped at once.
Does using a single lock always prevent deadlocks?
It removes the most common cause, since two held locks cannot form a cycle on their own. It does not make a deadlock impossible. One thread can still block on a lock while holding it, waiting on a database transaction, a file handle, or a pool slot that the rest of the system also contends for, and the thread-pool self-submission case deadlocks with a single lock in play. Treat a single lock as a strong default, not a guarantee.
How do I find a deadlock in a multithreaded program?
Take three stack dumps about ten seconds apart and compare them. Identical stacks on the blocked threads mean the wait is permanent. In Java, jstack or jcmd Thread.print prints a Found one Java-level deadlock section with the cycle spelled out when the JVM detects it, and otherwise the locked addresses on each thread let you build the wait-for graph by hand. In Python, py-spy dump –pid shows which threads are parked in acquire().
Are database transaction deadlocks different from thread deadlocks?
The mechanism is identical, but the recovery path is not. A deadlocked thread usually has done no external work yet, so you can kill it safely. A deadlocked transaction may be part way through a multi-statement unit, so the engine rolls back the victim and returns an error like 1213 on SQL Server or SQLSTATE 40P01 on PostgreSQL. The application must treat that error as normal and retry the transaction from the start.
What is the Banker’s algorithm?
It is an avoidance algorithm named after bankers who will not lend to someone who cannot repay. The system tracks available resources plus each process’s current allocation and maximum claim, then grants a request only if some process could still complete its work and release everything it holds. If every process can finish in some order, the state is safe. The catch is advance knowledge of maximum claims, which is why services rarely use it.
Conclusion
If a process is hung right now, here is the order I would work in. Take three stack dumps ten seconds apart and confirm the stacks are frozen rather than crawling. Read the wait-for graph off the locks each blocked thread holds and find the cycle; it is usually two threads, occasionally four, and rarely obvious from the code alone. Then fix the order: rank the locks, make every acquisition follow the ranking, and document it so the next person inherits it rather than invents a new one.
After that, shorten the critical sections and move every blocking call out of them, because that change pays off on every load level rather than only when a cycle forms. Add timeouts on the remaining acquisitions with backoff, so a rare residual cycle surfaces as a retryable error rather than a silent stall. And then actually test it under contention, with a load test that runs concurrent transactions on the same rows, because a lock-ordering bug is invisible to single-threaded runs and single-user staging.
Deadlocks are one of the few concurrency bugs that are entirely a design problem rather than a memory problem, which is good news. Change the structure and they do not happen.


