OS Concepts
Intermediate 10 min read

Deadlocks

In one line

A deadlock is a set of threads each waiting for a resource another one holds, so none can ever proceed; it needs four conditions, and breaking any one prevents it.

What it is

A deadlock happens when two or more threads (or processes, or database transactions) are stuck forever because each holds something the next one needs. Thread 1 holds lock A and waits for lock B, while Thread 2 holds lock B and waits for lock A.

Deadlock needs all four Coffman conditions at once: mutual exclusion (a resource can be held by only one at a time), hold and wait (a thread holds one resource while waiting for another), no preemption (resources cannot be taken away by force), and circular wait (a cycle of threads each waiting for the next). Related problems are livelock, where threads keep reacting to each other and make no progress, and starvation, where one thread is always passed over.

Why interviewers ask

Deadlocks freeze services and databases in production, often only under heavy load. Interviewers expect you to name the four conditions, explain prevention (especially lock ordering), and know how real systems detect and recover, for example databases aborting one transaction. It shows you can design concurrent code that stays correct, not just fast.

How it works

  1. 1Model it as a wait-for graph: an arrow from T1 to T2 means T1 is waiting for something T2 holds. With single-instance resources, a cycle in this graph means deadlock.
  2. 2Prevention breaks one of the four conditions by design. The most practical is breaking circular wait: always acquire locks in a fixed global order (for example by account id). Other options: request all resources at once (breaks hold and wait), or allow preemption, such as try-lock with timeout and release everything on failure.
  3. 3Avoidance checks each request before granting it and only grants it if the system stays in a safe state. The Banker's algorithm is the textbook example; it needs to know each process's maximum needs in advance, so it is rarely used in general-purpose systems.
  4. 4Detection and recovery lets deadlocks happen, periodically finds cycles in the wait-for graph, then recovers by aborting or rolling back one victim. Databases do this.
  5. 5Ignoring it (the ostrich approach) is what most general-purpose operating systems do for application locks: it is left to the programmer.

Worked example · Two bank transfers in opposite directions

  1. Thread 1 runs transfer(A to B): it locks account A, then tries to lock B.
  2. At the same moment Thread 2 runs transfer(B to A): it locks account B, then tries to lock A.
  3. Thread 1 holds A and waits for B. Thread 2 holds B and waits for A. Wait-for graph: T1 to T2 to T1, a cycle. All four conditions hold, so both wait forever.
  4. Fix with lock ordering: always lock the account with the lower id first. Say A has id 1 and B has id 2.
  5. Now Thread 1 locks A (id 1) then B. Thread 2 also locks A first, not B. Whichever thread gets A first proceeds and takes B; the other waits on A while holding nothing.
  6. There is no cycle, so no deadlock. Both transfers complete one after the other.

Picture it

Two cars meet on a narrow one-lane bridge from opposite ends. Each holds half the bridge and waits for the other to back up, and neither does. Lock ordering is a rule that cars always cross north to south first, so they never meet head on.

Reference code

import threading

class Account:
    def __init__(self, acc_id, balance):
        self.id = acc_id
        self.balance = balance
        self.lock = threading.Lock()

def transfer(src, dst, amount):
    # Always lock the lower id first. Every thread takes locks in the
    # same global order, so a waiting cycle (circular wait) is impossible.
    first, second = (src, dst) if src.id < dst.id else (dst, src)
    with first.lock:
        with second.lock:
            if src.balance < amount:
                return False
            src.balance -= amount
            dst.balance += amount
            return True

a, b = Account(1, 100), Account(2, 100)
t1 = threading.Thread(target=transfer, args=(a, b, 30))
t2 = threading.Thread(target=transfer, args=(b, a, 50))
t1.start(); t2.start(); t1.join(); t2.join()
print(a.balance, b.balance)      # 120 80, and it never hangs

Both transfers lock account 1 before account 2, regardless of direction, so a cycle in the wait-for graph is impossible. The final balances are 120 and 80 whichever thread runs first, since both transfers have enough funds.

Key patterns

Global lock ordering

When: Code that needs two or more locks

How: Define an order (by id, by address, by name) and always acquire in that order; this removes circular wait.

Try-lock with timeout and back off

When: Lock ordering is impractical

How: Use acquire(timeout=...); on failure release everything held, wait a random short time and retry. Randomness avoids livelock.

Acquire all at once

When: The set of resources is known up front

How: Request every resource together, or none, which breaks hold and wait.

Detect and retry

When: Databases and transactional systems

How: Let the system abort a victim and retry the transaction in application code.

Real-world use

PostgreSQL deadlock detection

When a transaction waits on a lock longer than deadlock_timeout (1 second by default), PostgreSQL checks for a cycle and, if it finds one, aborts one transaction with a 'deadlock detected' error so the others continue.

MySQL InnoDB

InnoDB detects deadlocks as they form and rolls back the transaction it considers smallest; applications are expected to retry. It also has innodb_lock_wait_timeout for long lock waits.

Java thread dumps

The jstack tool and JVM thread dumps report 'Found one Java-level deadlock' with the threads and monitors involved, which is how many production hangs are diagnosed.

Distributed locks

Systems using Redis or ZooKeeper locks give locks a lease or timeout, so a crashed holder cannot block everyone forever, effectively adding preemption.

In a project

A payments service logs a few hundred 'deadlock detected' errors a day from PostgreSQL during peak hours, each causing a failed transfer.

  1. Read the database logs, which list the queries and locks in each deadlock; find that transfers lock the two account rows in request order (sender first).
  2. Change the code to lock both rows in id order, for example SELECT ... FROM accounts WHERE id IN (a, b) ORDER BY id FOR UPDATE.
  3. Keep transactions short: no external API calls while holding row locks.
  4. Add an automatic retry with backoff for the rare remaining deadlock or serialization errors.
  5. Track deadlock count per day on a dashboard.

Impact: Deadlocks drop from hundreds a day to near zero, failed transfers disappear from user reports, and p99 latency improves because transactions no longer wait for the detector to break cycles.

Trade-offs

Use it when

  • Lock ordering: almost always the first choice for application code with multiple locks
  • Timeouts and retries: when you cannot control the order, or across services
  • Detection and recovery: databases and systems where transactions can be safely rolled back and retried

Avoid it when

  • The Banker's algorithm in general software: it needs maximum resource needs known in advance
  • Retries without backoff or jitter: threads can livelock, retrying in lockstep forever
  • Long transactions or holding locks during network calls: they make deadlocks and lock waits far more likely

Cost: Detecting a cycle in the wait-for graph is O(V + E) with a depth-first search.

Common mistakes

  • Listing the four conditions but not explaining that breaking any one prevents deadlock.
  • Confusing deadlock with starvation (one thread waits indefinitely while others progress) or livelock (threads are active but make no progress).
  • Proposing the Banker's algorithm as a practical fix for a web service.
  • Forgetting that databases deadlock too, and that application code must retry the aborted transaction.
  • Adding a timeout without a retry plan, so the operation just fails.

Interview questions

Quick revision

  • Four conditions: mutual exclusion, hold and wait, no preemption, circular wait.
  • Break any one to prevent deadlock.
  • Most practical fix: global lock ordering.
  • Alternatives: acquire all at once, try-lock with timeout and backoff.
  • Avoidance: Banker's algorithm (needs max demands up front).
  • Detection: cycle in wait-for graph, abort a victim (databases).
  • Livelock = busy but stuck; starvation = one waits forever.

Check yourself

1. Which is NOT one of the four Coffman conditions for deadlock?

2. Always acquiring locks in increasing id order prevents deadlock by breaking which condition?

3. PostgreSQL detects a deadlock between two transactions. What happens?

4. Two threads keep backing off and retrying at exactly the same moments, so neither gets both locks, though both are running. What is this?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free