Loading content...
Deadlocks are the nemesis of lock-based concurrency control. Two or more transactions become permanently stuck, each waiting for the other to release locks. Without intervention, the system grinds to a halt.
Deadlock problems are interview favorites because they test your understanding of concurrency, graph algorithms, and system design trade-offs. You'll be asked to detect deadlocks, explain prevention strategies, and reason about which transaction to abort.
This page gives you complete mastery of deadlock theory and practice, preparing you to handle any deadlock-related interview question with confidence.
By completing this page, you will be able to: (1) Understand the four necessary conditions for deadlock; (2) Construct wait-for graphs and detect cycles; (3) Apply deadlock prevention techniques (wound-wait, wait-die); (4) Analyze deadlock avoidance strategies; (5) Choose optimal victim transactions for deadlock resolution.
A deadlock is a situation where a set of transactions are blocked forever, each waiting for a lock held by another transaction in the set. No transaction can proceed, and without intervention, they will wait indefinitely.
T₁: holds X(A), waiting for X(B)
T₂: holds X(B), waiting for X(A)
T₁ cannot proceed until T₂ releases B. T₂ cannot proceed until T₁ releases A. Neither can make progress—they're deadlocked.
For a deadlock to occur, ALL four conditions must hold simultaneously:
Breaking any one condition prevents deadlock. Different strategies target different conditions:
In a deadlock, transactions are blocked and do nothing. In a livelock, transactions keep running (aborting and retrying) but make no progress. Both are problematic, but they require different solutions.
The wait-for graph is the standard tool for deadlock detection. It's analogous to the precedence graph for serializability—if there's a cycle, there's a problem.
Step 1: Create Nodes
Step 2: Create Edges
Step 3: Detect Cycles
Current state:
• T₁ holds X(A), waiting for X(B)
• T₂ holds X(B), X(C), waiting for X(D)
• T₃ holds X(D), waiting for X(A)
• T₄ holds X(E), waiting for X(C)Wait-For Graph Edges:
• T₁ → T₂ (T₁ waits for B, held by T₂)
• T₂ → T₃ (T₂ waits for D, held by T₃)
• T₃ → T₁ (T₃ waits for A, held by T₁)
• T₄ → T₂ (T₄ waits for C, held by T₂)
Graph:
T₁ → T₂ → T₃ → T₁ (CYCLE!)
T₄ → T₂
✓ Deadlock detected: T₁, T₂, T₃ are in a deadlock cycle
✓ T₄ is only blocked, not deadlocked (will proceed once T₂ releases C)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
def build_wait_for_graph(lock_state, wait_requests): """ Build wait-for graph from current lock state and pending requests. Args: lock_state: Dict[item] -> transaction holding the lock wait_requests: List[(waiting_txn, requested_item)] Returns: Adjacency list of wait-for relationships """ from collections import defaultdict graph = defaultdict(set) for waiting_txn, item in wait_requests: if item in lock_state: holding_txn = lock_state[item] if waiting_txn != holding_txn: graph[waiting_txn].add(holding_txn) return graph def detect_deadlock(graph, transactions): """ Detect if deadlock exists using DFS cycle detection. Returns: (has_deadlock, cycle_transactions) """ WHITE, GRAY, BLACK = 0, 1, 2 color = {t: WHITE for t in transactions} def dfs(node, path): color[node] = GRAY for neighbor in graph.get(node, []): if color[neighbor] == GRAY: # Found cycle - return transactions in cycle cycle_start = path.index(neighbor) return True, path[cycle_start:] if color[neighbor] == WHITE: result = dfs(neighbor, path + [neighbor]) if result[0]: return result color[node] = BLACK return False, [] for t in transactions: if color[t] == WHITE: result = dfs(t, [t]) if result[0]: return result return False, [] # Example usagelock_state = {'A': 'T1', 'B': 'T2', 'C': 'T2', 'D': 'T3', 'E': 'T4'}wait_requests = [('T1', 'B'), ('T2', 'D'), ('T3', 'A'), ('T4', 'C')]transactions = {'T1', 'T2', 'T3', 'T4'} graph = build_wait_for_graph(lock_state, wait_requests)has_deadlock, cycle = detect_deadlock(graph, transactions) print(f"Deadlock: {has_deadlock}") # Trueprint(f"Cycle: {cycle}") # ['T1', 'T2', 'T3'] or similarA wait-for graph can have multiple cycles. Standard DFS will find one cycle, but resolving it (by aborting a transaction) may reveal additional cycles that were hidden. Real systems run cycle detection iteratively until no cycles remain.
Prevention strategies ensure deadlocks never occur by breaking one of the four necessary conditions before any transaction enters a wait state.
Idea: Define a global ordering on data items. Transactions must acquire locks in that order.
Example: If the ordering is A < B < C, then any transaction needing locks on A and C must request A first, then C. Never C before A.
Effect: Prevents circular wait—any waiting chain follows the total order, so cycles are impossible.
Limitation: Requires knowing all locks needed in advance; not always practical.
Idea: Each transaction declares all locks it will need before starting. System either grants all or none.
Effect: Prevents hold-and-wait—transactions never hold locks while waiting.
Limitation: Many applications don't know lock needs in advance.
These protocols use transaction timestamps to make consistent decisions about who waits and who aborts. They prevent deadlocks by ensuring wait relationships never form cycles.
Rule: "Older waits for younger; younger dies"
Result: Wait edges only go from older → younger, so no cycle possible.
Rule: "Older wounds younger; younger waits"
Result: Wait edges only go from younger → older, so no cycle possible.
| Scenario | Wait-Die | Wound-Wait |
|---|---|---|
| Older requests from younger | Older waits | Older wounds (aborts younger) |
| Younger requests from older | Younger dies (aborts) | Younger waits |
| Who gets aborted | Younger transactions | Younger transactions |
| Who waits | Older transactions | Younger transactions |
| Preemptive | No (requester may abort) | Yes (holder may be aborted) |
| Advantage | Less disruption (holder never aborted) | No starvation (older always wins) |
When a transaction aborts and restarts under these protocols, it KEEPS its original timestamp. This prevents starvation—eventually, the transaction becomes 'old' enough to succeed without being aborted.
Avoidance strategies allow deadlock conditions to exist but carefully choose which lock requests to grant to ensure a deadlock never actually forms.
Classic avoidance: Before granting a lock, simulate what happens. If granting could lead to a state where deadlock is inevitable, don't grant (force transaction to wait).
In database terms: Before T_i acquires lock on X, check if a safe sequence exists—a sequence in which all transactions can still complete.
A state is safe if there exists some order to complete all current transactions, granting their remaining lock requests, without deadlock.
Example:
System resources: 10 units total
T₁: holds 5, needs max 8 → still needs 3
T₂: holds 3, needs max 9 → still needs 6
Available: 2 units
Can we complete?
- Give T₁ remaining 2 → T₁ has 7 (not enough for max 8) → Wait
- Actually: T₁ needs 3 more, we have 2 → If nothing else, unsafe?
Let's think differently:
- Available: 2, T₁ needs 3 more → Cannot complete T₁ first
- Available: 2, T₂ needs 6 more → Cannot complete T₂ first
→ UNSAFE STATE (potential deadlock)
A simpler, practical approach used in most real systems:
Mechanism: If a transaction waits for a lock longer than a threshold (e.g., 5 seconds), assume potential deadlock and abort the transaction.
Advantages:
Disadvantages:
Despite their theoretical elegance, sophisticated avoidance algorithms are rarely used in production databases. Simple timeouts combined with occasional wait-for graph checks are the norm. The overhead of perfect avoidance usually isn't worth it.
Detection strategies allow deadlocks to occur, then detect and break them. This is the most common approach in modern database systems.
Continuous detection: Check for deadlocks every time a wait occurs.
Periodic detection: Check at regular intervals (e.g., every 1 second).
Triggered detection: Check only when symptoms appear (e.g., many transactions waiting).
Once a deadlock is detected, at least one transaction must be aborted (rolled back) to break the cycle. The aborted transaction is called the victim.
Total Rollback: Abort the victim completely and restart from the beginning.
Partial Rollback: Roll back the victim just enough to release the lock causing the deadlock.
If the same transaction is repeatedly chosen as victim, it may never complete (starvation).
Solution: Track how many times a transaction has been victimized. Include this count in victim selection—after N times, give the transaction priority immunity.
When asked about deadlock resolution, structure your answer: (1) Detect via wait-for graph (show cycle), (2) Explain victim selection heuristics, (3) Discuss total vs. partial rollback, (4) Mention starvation prevention. This demonstrates comprehensive understanding.
Let's work through several complete deadlock analysis problems.
Problem: Given the following schedule, determine when/if a deadlock occurs.
Time T₁ T₂ T₃
1 X(A)
2 X(B)
3 X(C)
4 req X(B)
[wait]
5 req X(C)
[wait]
6 req X(A)
[wait]
Analysis:
After time 6:
Wait-for graph: T₁ → T₂ → T₃ → T₁ (CYCLE)
Result: Deadlock detected at time 6 when T₃ requests X(A).
Resolution: Abort one transaction (e.g., T₃ as it has done least work if we choose that criterion), release X(C). T₂ gets X(C), completes, releases X(B). T₁ gets X(B), completes.
Deadlock problems in interviews follow recognizable patterns. Here's how to approach each type systematically.
(1) In Wait-Die/Wound-Wait, transactions restart with the SAME timestamp. (2) Younger means higher timestamp (started later). (3) Wait-Die never aborts the holder; Wound-Wait can. (4) A long wait isn't deadlock if no cycle exists—it's just blocking.
Deadlock handling is essential for any lock-based concurrency control system. Let's consolidate the essential skills:
What's Next:
With deadlock detection mastered, we'll tackle Recovery Scenarios—understanding how database systems recover from failures, the role of logging, ARIES recovery algorithm, and how to reason about recovery-related interview problems.
You now have comprehensive knowledge of deadlock theory and practice. You can construct wait-for graphs, apply prevention protocols, select optimal victims, and reason about the trade-offs between different deadlock handling strategies. This knowledge is directly applicable to system design interviews and real database implementation.