Loading learning content...
Imagine a traffic intersection where four cars arrive simultaneously from four directions. Each car needs to cross, but each is blocked by the car to its right. No car can move; no car will yield. They wait forever. This is a deadlock.
In operating systems, deadlocks occur when processes hold resources while waiting for other resources held by other waiting processes, forming a circular dependency that can never resolve. Understanding deadlock—its causes, conditions, prevention, and resolution—is fundamental to operating systems knowledge and a frequent interview topic.
The deceptive simplicity of the question "What are the conditions for deadlock?" belies the depth required for a complete answer. This page provides that depth.
By the end of this page, you will understand: (1) the precise definition of deadlock and its relationship to resource allocation, (2) the four necessary conditions (Coffman conditions) and why each is necessary, (3) strategies for prevention, avoidance, detection, and recovery, (4) real-world examples and their solutions, and (5) how to articulate this knowledge clearly in interviews.
A deadlock is a state in which a set of processes are blocked because each process is holding a resource and waiting for a resource held by another process in the set. The processes are stuck in a circular wait, and without external intervention, they will remain blocked forever.
A set of processes {P₁, P₂, ..., Pₙ} is deadlocked if:
To analyze deadlock precisely, we model the system as:
Resources: Items that processes need and must request before use
Resource types: Categories of identical resources
Processes: Active entities that request, use, and release resources
Resource allocation: The mapping of resources to processes
1234567891011121314151617181920212223242526
Resource-Allocation Graph: Nodes: - Process nodes: P₁, P₂, P₃ (circles) - Resource nodes: R₁, R₂, R₃ (squares with dots for instances) Edges: - Request edge: Pᵢ → Rⱼ (process wants resource) - Assignment edge: Rⱼ → Pᵢ (resource held by process) Example (Deadlock): ┌──────────────────────────────────┐ │ wants │ ▼ │ ┌───┐ ┌───┐ ┌───┐ │ │ P₁ │ ──────→ │ R₂ │ ──────→ │ P₂ │ ──┘ └───┘ wants └───┘ held by └───┘ ▲ │ │ held by │ wants ┌───┐ ┌───┐ │ R₁ │ ←───────────────────── │ R₁ │ └───┘ └───┘ Cycle: P₁ → R₂ → P₂ → R₁ → P₁This cycle indicates deadlock (with single-instance resources).Deadlock vs. Livelock:
Deadlock vs. Starvation:
Deadlock vs. Race Condition:
In 1971, E.G. Coffman, M.J. Elphick, and A. Shoshani identified four conditions that must all hold simultaneously for deadlock to occur. These are the Coffman conditions:
Statement: At least one resource must be held in a non-sharable mode. Only one process can use the resource at a time.
Explanation: If resources could be shared by all processes simultaneously, no process would need to wait. Mutual exclusion creates the possibility of waiting.
Examples:
Why it's necessary: Without mutual exclusion, there's no waiting—processes access resources freely. No waiting means no deadlock.
Whether mutual exclusion applies depends on the resource. Read-only files can be shared; write access requires exclusion. CPUs can be preempted; printers cannot midway through a job. Understanding which resources truly require exclusion is key to deadlock analysis.
Statement: A process must be holding at least one resource and waiting to acquire additional resources currently held by other processes.
Explanation: Processes don't just wait—they wait while holding. This "holding" is what creates the blocking chain.
Examples:
Why it's necessary: If processes released everything before waiting for more, no process would block another. The "hold" part enables blocking.
Statement: Resources cannot be forcibly taken from a process; they must be released voluntarily by the holder.
Explanation: If the system could take resources away, it could break any blocking chain. Non-preemptability means waits can last indefinitely.
Examples:
Why it's necessary: If resources could be preempted, the OS could break cycles by taking resources from one process and giving to another.
Statement: There must exist a set of waiting processes {P₀, P₁, ..., Pₙ} such that P₀ waits for a resource held by P₁, P₁ waits for a resource held by P₂, ..., and Pₙ waits for a resource held by P₀.
Explanation: The circular dependency is the "trap" that makes deadlock permanent. Without a cycle, eventually some process completes and releases resources.
Example:
P₀ holds R₀, wants R₁
P₁ holds R₁, wants R₂
P₂ holds R₂, wants R₀
Cycle: P₀ → R₁ → P₁ → R₂ → P₂ → R₀ → P₀
Why it's necessary: Circular wait is the defining characteristic. A wait chain that doesn't form a cycle will eventually resolve.
| Condition | Description | Key Insight |
|---|---|---|
| Mutual Exclusion | Resources cannot be shared | Creates the need to wait |
| Hold and Wait | Processes hold resources while waiting for more | Enables blocking chains |
| No Preemption | Resources cannot be forcibly taken | Waits can be indefinite |
| Circular Wait | Circular chain of processes waiting | Traps processes in loop |
Deadlock requires ALL FOUR conditions simultaneously. Breaking any one condition prevents deadlock. Most deadlock prevention strategies focus on eliminating exactly one condition—the easiest one to remove given the system's constraints.
Let's trace a deadlock scenario in detail to see all four conditions in action.
Five philosophers sit at a round table. Between each pair of philosophers is a single chopstick (5 chopsticks total). Philosophers alternate between thinking and eating. To eat, a philosopher needs both chopsticks adjacent to them.
The problematic algorithm:
while (true) {
think();
pick_up(left_chopstick);
pick_up(right_chopstick);
eat();
put_down(right_chopstick);
put_down(left_chopstick);
}
Deadlock scenario:
12345678910111213141516171819202122232425262728
P₀ (has C₀, wants C₁) ↓ ┌─────┐ │ C₀ │━━━━━━━━━━━━━━━━━━━━┓ └─────┘ ┃ ↑ ▼ P₄ (has C₄, wants C₀) ┌─────┐ │ C₁ │ └─────┘ ↓ ┌─────┐ P₁ (has C₁, wants C₂) │ C₄ │ └─────┘ ↓ ↑ ┌─────┐ ┃ │ C₂ │ P₃ (has C₃, wants C₄) └─────┘ ↑ ↓ ┌─────┐ P₂ (has C₂, wants C₃) │ C₃ │ ←────────────────┛ └─────┘ Cycle: P₀→C₁→P₁→C₂→P₂→C₃→P₃→C₄→P₄→C₀→P₀ All four conditions:1. Mutual Exclusion: Each chopstick used by one philosopher at a time2. Hold and Wait: Each holds left chopstick, waits for right3. No Preemption: Cannot take chopstick from philosopher mid-meal4. Circular Wait: Shown in the diagram aboveDatabase transactions frequently encounter deadlock potential:
Transaction T₁:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A'; -- Locks row A
UPDATE accounts SET balance = balance + 100 WHERE id = 'B'; -- Wants lock on row B
COMMIT;
Transaction T₂:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 'B'; -- Locks row B
UPDATE accounts SET balance = balance + 50 WHERE id = 'A'; -- Wants lock on row A
COMMIT;
Timeline:
Time 1: T₁ acquires lock on row A
Time 2: T₂ acquires lock on row B
Time 3: T₁ requests lock on row B → BLOCKED (T₂ holds it)
Time 4: T₂ requests lock on row A → BLOCKED (T₁ holds it)
Deadlock! Neither can proceed.
Database systems detect this via wait-for graphs and abort one transaction (the victim), allowing the other to complete.
Deadlock prevention ensures that at least one of the four necessary conditions can never hold. The system is designed to make deadlock structurally impossible.
Approach: Allow resources to be shared rather than exclusive.
Applicability: Limited. Some resources are inherently non-sharable:
Example: Instead of directly accessing a printer, processes spool jobs to a queue. The spooler has exclusive access; processes don't wait.
Limitation: Cannot apply to intrinsically mutually exclusive resources.
Approach: Require processes to request all resources before execution, or release all before requesting more.
Technique A: Request all resources at once before starting
request(R1, R2, R3); // Atomic: get all or get none
// Process can only run if all resources granted
Technique B: Release everything before new requests
release(R1);
request(R2, R1); // Request again after releasing
Advantages: Simple to implement; guaranteed no deadlock.
Disadvantages:
Approach: If a process requests resources it cannot get, preempt (forcibly take) resources from it or from blocking processes.
Technique: If Process P requests resource R (held by Q):
Applicability: Only works for resources whose state can be saved and restored:
Example: Memory preemption via swapping. If a process needs memory held by another, pages can be swapped out.
Limitation: Most synchronization resources (locks, semaphores) cannot be preempted without causing inconsistency.
Approach: Impose a total ordering on resource types. Processes must request resources in increasing order.
Technique:
Example:
lock(mutex_1); // OK: 1 > nothing
lock(mutex_2); // OK: 2 > 1
// lock(mutex_1); // FORBIDDEN: 1 < 2 (already hold 2)
unlock(mutex_2);
unlock(mutex_1);
Why it works: If all processes request in the same order, cycles cannot form. P₁ waiting for resource with higher number can never be blocked by process waiting for lower number.
123456789101112131415161718192021222324
// Deadlock-prone code (inconsistent ordering):// Thread 1: // Thread 2:// lock(A); lock(B); // lock(B); lock(A); <-- DANGER // Deadlock-free code (consistent ordering):// Define order: A < B (A has lower number, must be acquired first) void transfer(Account* from, Account* to, int amount) { // Always lock accounts in consistent order (e.g., by address or ID) Account* first = (from < to) ? from : to; Account* second = (from < to) ? to : from; lock(first->mutex); // Lower-numbered resource first lock(second->mutex); // Higher-numbered resource second from->balance -= amount; to->balance += amount; unlock(second->mutex); unlock(first->mutex);} // Now Thread 1 and Thread 2 both acquire locks in the same order// No cycle can form; deadlock is impossibleLock ordering is the most practical prevention technique for application-level deadlock. Many systems establish conventions: acquire locks in order of memory address, resource ID, or hierarchical level. The Linux kernel maintains strict locking hierarchies; lock ordering violations are detected by lockdep during development.
Deadlock avoidance allows the system to dynamically decide whether to grant resource requests based on current state. Requests are granted only if the system can remain in a "safe state."
A state is safe if the system can allocate resources to each process (up to its maximum need) in some order and avoid deadlock.
Safe state: There exists a sequence of processes <P₁, P₂, ..., Pₙ> such that:
Unsafe state: No safe sequence exists. Deadlock may (but does not necessarily) occur.
Key insight: Safe → Might deadlock? No. Unsafe → Might deadlock? Yes. Being in an unsafe state doesn't mean deadlock has occurred, but it means deadlock is possible based on future requests.
The Banker's Algorithm (Dijkstra, 1965) is the classic deadlock avoidance algorithm. It's named after the way a banker must manage limited capital.
Prerequisites:
Data structures:
Available[m]: Available resources of each typeMax[n][m]: Maximum claim of each processAllocation[n][m]: Resources currently allocatedNeed[n][m]: Remaining needs (Max - Allocation)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Safety Algorithm: Determines if system is in safe statebool is_safe_state() { bool finish[n] = {false}; // Has process finished? int work[m]; // Working copy of available memcpy(work, available, sizeof(work)); while (true) { // Find a process that can finish bool found = false; for (int i = 0; i < n; i++) { if (!finish[i] && need[i] <= work) { // Process i can finish: its needs can be met work += allocation[i]; // Release its resources finish[i] = true; found = true; } } if (!found) break; // No progress possible } // Safe if all processes can finish for (int i = 0; i < n; i++) { if (!finish[i]) return false; // Unsafe: process i cannot finish } return true; // Safe: all processes can eventually finish} // Resource Request Algorithmbool request_resources(int process_id, int request[]) { if (request > need[process_id]) { return false; // Error: exceeds maximum claim } if (request > available) { return false; // Cannot grant now: not enough available } // Simulate granting the request available -= request; allocation[process_id] += request; need[process_id] -= request; // Check if resulting state is safe if (is_safe_state()) { return true; // Grant request } else { // Rollback: would result in unsafe state available += request; allocation[process_id] -= request; need[process_id] += request; return false; // Deny request (process must wait) }}Consider a system with 3 resource types (A, B, C) and 5 processes:
Available: [3, 3, 2]
Allocation Max Need
P0 [0, 1, 0] [7, 5, 3] [7, 4, 3]
P1 [2, 0, 0] [3, 2, 2] [1, 2, 2]
P2 [3, 0, 2] [9, 0, 2] [6, 0, 0]
P3 [2, 1, 1] [2, 2, 2] [0, 1, 1]
P4 [0, 0, 2] [4, 3, 3] [4, 3, 1]
Safety check:
Safe sequence: <P1, P3, P4, P0, P2>. System is in safe state.
The Banker's Algorithm requires knowing maximum resource needs in advance—often impractical. It's computationally expensive (O(n²m) per request). It's primarily of theoretical importance; real systems typically use prevention or detection instead.
Deadlock detection allows deadlock to occur, then detects and recovers from it. This approach doesn't restrict resource usage patterns.
When each resource type has only one instance, detection uses a wait-for graph:
Algorithm: Periodically run cycle detection on the wait-for graph.
With multiple instances per resource type, we need a more sophisticated algorithm:
123456789101112131415161718192021222324252627282930313233343536
// Detection algorithm for multiple-instance resources// Similar to safety algorithm, but uses current requests, not max claims bool detect_deadlock() { bool finish[n]; int work[m]; // Initialize: mark processes with no allocations as "finished" for (int i = 0; i < n; i++) { finish[i] = (allocation[i] == 0); // No resources → cannot be deadlocked } memcpy(work, available, sizeof(work)); while (true) { bool found = false; for (int i = 0; i < n; i++) { if (!finish[i] && request[i] <= work) { // Process i's current request can be satisfied work += allocation[i]; // Assume it finishes and releases finish[i] = true; found = true; } } if (!found) break; } // Deadlock: any process with finish[i] == false is deadlocked bool deadlocked = false; for (int i = 0; i < n; i++) { if (!finish[i]) { printf("Process %d is deadlocked\n", i); deadlocked = true; } } return deadlocked;}Options:
Tradeoff: More frequent detection finds deadlocks faster but consumes CPU cycles.
Databases are the most common place where deadlock detection runs actively:
Oracle: Checks every 3 seconds
MySQL/InnoDB: Checks after every lock wait
PostgreSQL: Checks when configurable deadlock_timeout expires
When deadlock is detected, one transaction is selected as a victim and rolled back.
Once deadlock is detected, the system must recover. Several strategies exist:
Terminate all deadlocked processes:
Terminate one process at a time:
Victim selection criteria:
Take resources from deadlocked processes:
Challenges:
Checkpoint and rollback:
Requirement: System must support checkpointing—common in databases, rare in general OS.
| Strategy | Pros | Cons | Use Case |
|---|---|---|---|
| Kill all | Simple, guaranteed | Loses all work | Simple embedded systems |
| Kill one at a time | Less loss | Repeated detection | General purpose |
| Resource preemption | Minimal disruption | Complex, not always possible | Specialized resources |
| Rollback to checkpoint | Recoverable, fair | Requires checkpointing | Databases, VMs |
1234567891011121314151617181920212223
-- When database detects deadlock: -- 1. Identify deadlocked transactions (e.g., T1 and T2) -- 2. Select victim (lowest cost to abort)-- - Fewest locks held-- - Least work done-- - Least number of undo operations -- 3. Abort victim transactionROLLBACK TRANSACTION T1; -- 4. Release all locks held by T1-- 5. T2 can now proceedCOMMIT TRANSACTION T2; -- 6. T1 must retry from beginningBEGIN TRANSACTION;-- T1's SQL statements again...COMMIT; -- Note: Application must handle "deadlock victim" errors-- and retry the transactionReal systems typically combine multiple deadlock handling strategies, applying different approaches to different resource types.
Approach: Ignore the problem entirely. Pretend deadlock doesn't happen.
Rationale:
Usage: Unix/Linux historically used this for many resources. Windows similarly. Web servers often rely on request timeouts.
When appropriate:
Modern systems use different strategies for different resource classes:
The "right" deadlock strategy depends on context. Databases can't ignore deadlock (data integrity matters). Desktop OS can often ignore it (users restart). Embedded systems might prevent entirely (strict static allocation). Real-time systems may use avoidance (predictable response required). There is no universal answer.
When asked "What are the conditions for deadlock?" or "How do you handle deadlock?", structure your answer to demonstrate comprehensive understanding.
Strong answers: (1) Know all four conditions by name and meaning, (2) Understand why ALL are necessary, (3) Can explain prevention techniques for each condition, (4) Know the difference between prevention and avoidance, (5) Can discuss real-world approaches (databases detect/rollback, kernels prevent with lock ordering, general OS ignores).
Deadlock is a fundamental challenge in concurrent systems. Let's consolidate the key insights:
What's Next:
Having mastered deadlock theory, we'll move to the next essential interview topic: File System Comparisons. Understanding the tradeoffs between different file system designs—journaling, copy-on-write, log-structured—demonstrates systems thinking that interviewers value.
You now possess complete knowledge of deadlock theory—from the four conditions through prevention, avoidance, detection, and recovery. This understanding applies to operating systems, databases, distributed systems, and any concurrent programming context.