Deadlock: Definition and Conditions

Imagine two threads, each holding a lock the other needs, both waiting forever. Neither will ever release what it has. So neither can proceed. This scenario, a deadlock, is one of the oldest and most persistent hazards in concurrent systems. It costs real money: a stuck database transaction, a frozen operating system driver, a hung distributed job scheduler. Understanding deadlock precisely, not just as "things got stuck," is essential to reasoning about concurrent programs at all. The problem matters because modern software is saturated with concurrency: multi-threaded servers, multi-process operating systems, distributed transactions across databases, and even hardware bus arbitration. Whenever multiple independent activities compete for a finite pool of reusable resources — locks, memory buffers, printers, database rows, network connections — the possibility of a circular wait among them is lurking. Unlike a crash, a deadlock does not announce itself with an error message. The system simply stops making progress, often silently. Diagnosing it after the fact can be extremely difficult. The big picture is that deadlock is not a vague notion of "bad concurrency." It is a precisely characterizable state, definable by four necessary conditions (the Coffman conditions). It is also visualizable exactly through a resource-allocation graph (RAG). This graph turns an abstract liveness problem into a concrete graph-theory question: does a cycle exist, and if so, does that cycle actually force a deadlock? Building on ideas from Locks and Mutual Exclusion and the intuition from The Dining Philosophers Problem, this article gives you the vocabulary and the graph-based reasoning tools. Every subsequent deadlock technique — prevention, avoidance, detection — depends on them. This article delivers a rigorous definition of deadlock, and a derivation of the four necessary conditions. It presents the resource-allocation graph model with its cycle-detection algorithm. It includes a worked trace showing exactly how a cycle forms and is detected. It also offers a careful discussion of when a cycle does and does not imply deadlock. It closes by connecting these ideas forward to prevention, avoidance (the Banker's Algorithm), and the related but distinct failure modes of starvation and livelock.

Key takeaways
  • Deadlock: a set of processes each waiting for a resource held by another in the same set, forever.
  • Four Coffman conditions — mutual exclusion, hold-and-wait, no preemption, circular wait — are all necessary; together they are sufficient.
  • A resource-allocation graph models processes, resources, requests, and allocations as a directed graph.
  • A cycle in a RAG is necessary for deadlock; it is sufficient only when each resource type has a single instance.
  • Cycle detection in a RAG runs in O(V + E) time using standard graph traversal (DFS/BFS).
  • These conditions and graphs are the foundation for deadlock prevention, avoidance, and detection algorithms.
The core insight

Deadlock is exactly the simultaneous presence of mutual exclusion, hold-and-wait, no preemption, and circular wait — break any one and deadlock becomes impossible.

The idea

A deadlock is a situation in which every member of a set of processes (or threads) is blocked, waiting for an event — typically release of a resource — that only another member of the same set can cause. Because every member is blocked, no one can ever trigger the needed event, and the entire set is stuck permanently. This is distinct from a process merely waiting: waiting is normal and temporary, while deadlock is a structural impossibility of progress. Deadlock analysis concerns reusable resources: resources like locks, memory, CPUs, or file handles that a process acquires, uses, and eventually releases for reuse (as opposed to consumable resources like messages, which are produced and consumed once). A process interacting with a reusable resource follows a lifecycle: request the resource, use it, then release it. Deadlock arises from the interaction of multiple processes each following this lifecycle over multiple resources, where the order of acquisition creates a cyclic dependency. In 1971, E. G. Coffman, M. J. Elphick, and A. Shoshani identified four conditions that must all hold simultaneously for deadlock to be possible. These are individually necessary and jointly sufficient — meaning a deadlock cannot exist unless all four hold, and if all four hold along a cycle of waiting processes, deadlock exists. They give us a precise vocabulary for classifying and preventing deadlocks, foreshadowing the whole family of techniques in Deadlock Prevention and Avoidance.

How it works

The four Coffman conditions

1. Mutual Exclusion — At least one resource must be held in a non-shareable mode: only one process can use it at a time. (Read-only, shareable resources cannot participate in a deadlock cycle, since a process never needs to wait to acquire them.) 2. Hold and Wait — A process must be holding at least one resource while simultaneously waiting to acquire additional resources currently held by other processes. It does not release what it already has while it waits. 3. No Preemption — Resources cannot be forcibly taken away from a process. A resource can only be released voluntarily by the process holding it, after it has finished using it. 4. Circular Wait — There must exist a set of waiting processes {P0,P1,…,Pn}\{P_0, P_1, \dots, P_n\} such that P0P_0 waits for a resource held by P1P_1, P1P_1 waits for a resource held by P2P_2, ..., and PnP_n waits for a resource held by P0P_0. These four conditions are not independent design choices you can mix and match arbitrarily — they are a derivation: circular wait cannot occur without hold-and-wait (a process in the cycle must be holding something while waiting), hold-and-wait is meaningless without mutual exclusion (if resources could be shared freely, there would be nothing to "wait" for), and the whole cycle would resolve itself if preemption were allowed (the system could just yank a resource from one process to break the cycle). This is why eliminating any single one of the four is a valid deadlock-prevention strategy, foreshadowed here and developed fully under Deadlock Prevention and Avoidance.

The resource-allocation graph (RAG)

A resource-allocation graph is a directed graph G=(V,E)G = (V, E) used to represent the exact state of resource requests and allocations at an instant in time.

  • V=P∪RV = P \cup R, where P={P1,…,Pn}P = \{P_1, \ldots, P_n\} is the set of processes and R={R1,…,Rm}R = \{R_1, \ldots, R_m\} is the set of resource types.
  • A request edge Pi→RjP_i \rightarrow R_j means process PiP_i has requested an instance of resource RjR_j and is waiting.
  • An assignment edge Rj→PiR_j \rightarrow P_i means an instance of resource RjR_j has been allocated to PiP_i.
  • Each resource type RjR_j is drawn with a number of dots equal to its instance count.

Given this graph, two structural facts govern deadlock:

  • If the graph contains no cycle, there is no deadlock. (Necessity of a cycle.)
  • If every resource type in the graph has exactly one instance, then a cycle is both necessary and sufficient for deadlock.
  • If some resource type has multiple instances, a cycle is necessary but not sufficient — the cycle may be broken by another process outside the cycle eventually releasing a resource that satisfies a pending request.

Cycle detection algorithm

Detecting deadlock (under the single-instance-per-type simplification, or more generally checking the necessary condition) reduces to cycle detection in a directed graph, solvable by depth-first search in linear time. ``` function HAS_CYCLE(Graph G): color[v] ← WHITE for all v in V(G) # unvisited for each vertex v in V(G): if color[v] == WHITE: if DFS_VISIT(v): return true # cycle found return false function DFS_VISIT(v): color[v] ← GRAY # v is on the current recursion stack for each edge (v, u) in E(G): if color[u] == GRAY: return true # back edge -> cycle if color[u] == WHITE: if DFS_VISIT(u): return true color[v] ← BLACK # done with v, remove from stack return false ``` For the multi-instance case, a cycle alone is inconclusive; full deadlock detection requires a more general algorithm (structurally similar to the Banker's Algorithm's safety check) that tries to find a sequence of processes that can all finish given available resources — this is developed under later deadlock-detection material, but the graph model here is exactly what that algorithm consumes as input. ``` function MULTI_INSTANCE_DEADLOCK_CHECK(n processes, m resource types):

# Available[j] = free instances of resource R_j

# Allocation[i][j] = instances of R_j held by P_i

# Request[i][j] = instances of R_j currently requested by P_i Work ← copy of Available Finish[i] ← false for all i

# A process with zero current allocation trivially cannot be deadlocked for i in 1..n: if Allocation[i] is all zeros: Finish[i] ← true changed ← true while changed: changed ← false for i in 1..n: if not Finish[i] and Request[i] <= Work: # elementwise Work ← Work + Allocation[i] Finish[i] ← true changed ← true deadlocked ← { P_i : Finish[i] == false } return deadlocked ```

Figure
A resource-allocation graph with a cycle: single-instance resources force deadlock
P1 P2 R1 R2 P1 → R2 (request) R1 → P1 (assigned) P2 → R1 (request) R2 → P2 (assigned) Cycle: P1 → R2 → P2 → R1 → P1 ⇒ deadlock (single instance each)

Complexity & correctness

Time complexity of cycle detection. DFS-based cycle detection visits every vertex once and traverses every edge once, giving O(∣V∣+∣E∣)O(|V| + |E|) time, where ∣V∣=∣P∣+∣R∣|V| = |P| + |R| and ∣E∣|E| is the number of request/assignment edges. This is optimal: any correct algorithm must at least inspect every edge once, since omitting an edge could hide a cycle.

Space complexity. O(∣V∣)O(|V|) for the color array and the recursion stack in the worst case (a graph that is one long path/cycle), plus O(∣V∣+∣E∣)O(|V| + |E|) for the adjacency-list representation of the graph itself.

Correctness of DFS cycle detection. The invariant is: a vertex is colored GRAY exactly while it is an ancestor of the current DFS call on the recursion stack, and BLACK once it and all its descendants are fully explored. A back edge — an edge from the current vertex to a GRAY vertex — exists if and only if there is a path from that GRAY ancestor down to the current vertex and an edge back up to it, which is precisely a cycle. Standard graph theory (via DFS forest classification of tree/back/forward/cross edges) proves: a directed graph has a cycle iff a DFS forest of it contains a back edge. Hence `HAS_CYCLE` is correct.

Correctness of "cycle ⇒\Rightarrow deadlock" (single instance case). Suppose the RAG has a cycle P1→R1→P2→R2→⋯→Pk→Rk→P1P_1 \to R_1 \to P_2 \to R_2 \to \cdots \to P_k \to R_k \to P_1 and every RiR_i has exactly one instance. Each RiR_i is already allocated to Pi+1P_{i+1} (that's why Ri→Pi+1R_i \to P_{i+1} is an edge), so PiP_i's request for RiR_i can never be satisfied unless Pi+1P_{i+1} releases it — but Pi+1P_{i+1} is itself blocked waiting on Ri+1R_{i+1}, held by Pi+2P_{i+2}, and so on around the cycle. No process in the cycle can proceed, so none can release anything: this is exactly deadlock. The single-instance assumption is essential — it guarantees the resource PiP_i needs is entirely consumed by the very process blocking it, with no alternate instance available.

Why a cycle is not sufficient with multiple instances. If RjR_j has, say, 3 instances, a request edge Pi→RjP_i \to R_j can be satisfied by any of the 3 instances, including one held by a process outside the cycle. That outside process may finish and release its instance, breaking the deadlock even though the cycle persists in the graph. Hence the multi-instance detection algorithm above must simulate whether some execution order lets all processes finish (the `Work`/`Finish` simulation), not merely check for a cycle.

Honesty about tradeoffs. Cycle detection tells you a deadlock is possible or, in the single-instance case, definite; it does not tell you when it will occur, nor does it prevent it. Building a full RAG and re-checking it on every request is straightforward but can be costly if done naively on every single operation in a busy system — real systems trade off between checking eagerly (avoidance, higher runtime cost, discussed in the Banker's Algorithm) and checking periodically or only when suspicion is triggered (detection, cheaper but allows deadlocks to persist for a while).

Worked example

Consider three processes P1,P2,P3P_1, P_2, P_3 and three single-instance resources R1,R2,R3R_1, R_2, R_3 (e.g., three distinct locks in a database engine), with the following sequence of events:

StepEventEdge added/removedGraph state after step
1P1P_1 acquires R1R_1add R1→P1R_1 \to P_1{R1→P1}\{R_1{\to}P_1\}
2P2P_2 acquires R2R_2add R2→P2R_2 \to P_2{R1→P1, R2→P2}\{R_1{\to}P_1,\ R_2{\to}P_2\}
3P3P_3 acquires R3R_3add R3→P3R_3 \to P_3{R1→P1, R2→P2, R3→P3}\{R_1{\to}P_1,\ R_2{\to}P_2,\ R_3{\to}P_3\}
4P1P_1 requests R2R_2add P1→R2P_1 \to R_2no cycle yet (check via DFS: none found)
5P2P_2 requests R3R_3add P2→R3P_2 \to R_3still no cycle
6P3P_3 requests R1R_1add P3→R1P_3 \to R_1cycle formed: P1→R2→P2→R3→P3→R1→P1P_1{\to}R_2{\to}P_2{\to}R_3{\to}P_3{\to}R_1{\to}P_1

Tracing `HAS_CYCLE` at step 6, starting DFS from P1P_1: 1. Color P1P_1 GRAY. Follow edge P1→R2P_1 \to R_2. 2. Color R2R_2 GRAY. Follow edge R2→P2R_2 \to P_2. 3. Color P2P_2 GRAY. Follow edge P2→R3P_2 \to R_3. 4. Color R3R_3 GRAY. Follow edge R3→P3R_3 \to P_3. 5. Color P3P_3 GRAY. Follow edge P3→R1P_3 \to R_1. 6. Color R1R_1 GRAY. Follow edge R1→P1R_1 \to P_1. 7. P1P_1 is GRAY (already on the stack) ⇒\Rightarrow back edge detected ⇒\Rightarrow `HAS_CYCLE` returns true. Checking the four Coffman conditions confirms this is genuine deadlock, not just a cycle in a diagram:

  • Mutual exclusion: yes, each RiR_i is a single, non-shareable instance (e.g., an exclusive database lock).
  • Hold and wait: yes, e.g., P1P_1 holds R1R_1 while waiting for R2R_2.
  • No preemption: yes, assume the system never revokes a lock from a running transaction.
  • Circular wait: yes, exactly the cycle traced above.

All four hold, the cycle is confirmed by DFS in O(∣V∣+∣E∣)=O(6+6)=O(12)O(|V|+|E|) = O(6 + 6) = O(12) work, and since every resource has a single instance, the cycle is the deadlock — P1,P2,P3P_1, P_2, P_3 will wait forever. This is structurally identical to the classic circular lock-ordering bug: Thread 1: lock(A)→lock(B)\text{Thread 1: lock}(A)\to\text{lock}(B) while Thread 2: lock(B)→lock(A)\text{Thread 2: lock}(B)\to\text{lock}(A), the two-process special case of the same cycle. Now contrast with a multi-instance twist: suppose R1R_1 actually has 2 instances, one held by P1P_1 and a second held by some P4P_4 outside the cycle. The RAG still shows the same cycle among P1,P2,P3P_1, P_2, P_3, but P3P_3's request for R1R_1 can be satisfied the moment P4P_4 finishes and releases its instance — no deadlock actually occurs, even though `HAS_CYCLE` still reports true. This is exactly why the multi-instance simulation algorithm, not bare cycle detection, is required in general.

Common pitfalls & traps

PitfallWhy it's wrongFix
Treating "cycle in RAG" as always meaning deadlockOnly true when every resource type has one instanceRun the `Work`/`Finish` simulation for multi-instance resources
Confusing deadlock with starvationStarvation can resolve eventually; deadlock structurally cannotCheck whether all four Coffman conditions persist forever
Confusing deadlock with livelockIn livelock, processes are not blocked — they actively run but make no progress (e.g., repeatedly backing off)Distinguish "blocked forever" (deadlock) from "running forever without progress" (livelock)
Forgetting shareable resources can't cause deadlockRead-locks / shared resources never force a wait against each otherOnly model exclusive-access resources as deadlock-relevant in the RAG
Ignoring the order edges were added when reasoning about "who caused it"The cycle is a global property; no single edge is "the" bugAnalyze the whole cycle, not the last request that "triggered" the report
Assuming DFS from one start vertex suffices for disconnected graphsA cycle could exist in a different connected componentIterate `HAS_CYCLE` over all unvisited vertices, as the pseudocode does

Pioneers & history

Origins · 1 minEdward G. Coffman Jr. and how this was discovered
EG
Edward G. Coffman Jr.
1934– · Computer scientist, Columbia University and Bell Labs

Coffman's 1971 survey with Elphick and Shoshani, "System Deadlocks," formalized the four necessary conditions that now bear his name and gave operating-systems research a rigorous vocabulary for a problem that had previously been discussed only informally in early time-sharing and multiprogramming systems.

History
1965
— Dijkstra poses the Dining Philosophers Problem, exposing circular resource contention.
1971
— Coffman, Elphick, and Shoshani publish "System Deadlocks," defining the four necessary conditions.
1972
— Habermann and others extend resource-allocation-graph reasoning to formal deadlock-avoidance algorithms.
1975
— Widely deployed multiprogrammed operating systems adopt RAG-based deadlock detection in resource managers.
1980
s — Database systems generalize deadlock detection to distributed transaction graphs (wait-for graphs).

Where this matters

Deadlock reasoning underlies the design of virtually every multi-threaded and multi-process system. Database management systems (PostgreSQL, MySQL, Oracle) maintain internal wait-for graphs, a variant of the resource-allocation graph restricted to processes, and periodically run cycle detection to identify and abort a victim transaction. Operating-system kernels apply strict lock ordering conventions across subsystems (e.g., always acquire the scheduler lock before the memory-manager lock) specifically to eliminate the circular-wait condition by construction. Distributed systems (e.g., distributed lock managers in Google's Chubby or Apache ZooKeeper-based coordination) generalize the RAG across machines, where detecting cycles is harder because no single node sees the whole graph, motivating specialized distributed deadlock-detection protocols. Compilers and static analyzers for concurrent code (e.g., Rust's borrow checker influences, or tools like ThreadSanitizer's lock-order analyzers) attempt to statically rule out circular-wait patterns before deployment. All of this machinery — prevention strategies that eliminate one Coffman condition, avoidance strategies like the Banker's Algorithm that never allow an unsafe state, and detection strategies that periodically scan the RAG — builds directly on the definitions and graph model developed here, alongside the related but distinct hazards of Starvation and Livelock.

Ask the tutor: How does the multi-instance deadlock-detection algorithm's Work/Finish simulation differ in intent from the Banker's Algorithm's safety check?

Are all four Coffman conditions really necessary, or could deadlock occur with only three?

All four are strictly necessary; remove any one and deadlock becomes structurally impossible. Without mutual exclusion, no process ever needs to wait for exclusive access, so there is nothing to block on. Without hold-and-wait, a process that cannot get everything it needs immediately releases what it already holds, breaking any chain of dependency. Without no-preemption, the system can always forcibly reclaim a resource to unblock a waiting process, so no cycle can persist. Without circular wait, the waiting relationships form a directed acyclic graph, and by induction on that DAG some process must be waiting on nothing and can always proceed, which eventually frees resources for the rest. This is precisely why each condition is the target of a distinct prevention strategy.

What is the difference between a resource-allocation graph and a wait-for graph?

A resource-allocation graph includes both processes and resource nodes, with request edges (process to resource) and assignment edges (resource to process). A wait-for graph is a simplification used mainly for single-instance resources: it collapses out the resource nodes entirely, drawing a direct edge Pi→PjP_i \to P_j whenever PiP_i waits for a resource currently held by PjP_j. This collapsing is valid precisely because with single-instance resources, each resource maps to exactly one holder, so the resource node carries no extra information. Wait-for graphs are common in database systems because transactions, not resources, are what get aborted to break a cycle.

Does deadlock require exactly the same processes to be in a cycle, or can more than one cycle exist?

A resource-allocation graph can contain multiple, possibly overlapping, cycles simultaneously, each representing its own circular-wait relationship. In the single-instance case, every process in every cycle is deadlocked, and cycles can share vertices — a process may be a member of more than one cycle at once. This does not change the detection procedure: `HAS_CYCLE` only needs to find at least one cycle to confirm the graph is not deadlock-free, though a full analysis (as done in deadlock-recovery systems) typically needs to enumerate all cycles to decide which processes or resources to target for resolution.

How does the multi-instance deadlock-detection algorithm relate to the Banker's Algorithm?

Both algorithms are built around the same core simulation: track a `Work` vector of currently available resources, and repeatedly try to find a process whose remaining `Request` (or, in the Banker's Algorithm, `Need`) can be satisfied by `Work`, then pretend it finishes and add its resources back. The difference is timing and intent. The Banker's Algorithm runs this simulation before granting a request, to decide if the resulting state is safe (avoidance). The deadlock-detection algorithm shown here runs the same style of simulation after the fact, on the current actual state, to determine which processes, if any, are already stuck. This shared machinery is why the Banker's Algorithm is introduced right after deadlock's basic definitions.

Can deadlock happen with just a single resource type that has many instances?

Yes, if processes hold-and-wait for more units of that same resource type than remain available, forming a cycle through multiple instances of one resource node. For example, if a resource type has 4 instances and three processes each hold 2 and each additionally request 2 more, no process can get enough units to finish, and none can release resources, since each is stuck waiting. The resource-allocation graph would show multiple assignment edges into the resource node and multiple request edges out of it. The general multi-instance detection algorithm handles this correctly by tracking total counts per resource type rather than per-instance identity.

Is a livelocked system also in a Coffman-condition state?

No — this is a key distinction. In livelock, processes are actively executing instructions, not blocked, but their state changes never lead to progress (a classic example is two threads each repeatedly yielding to avoid collision, forever colliding again). The Coffman conditions specifically describe blocked, waiting processes holding resources; livelocked processes are typically not holding resources exclusively while waiting in the deadlock sense; they are consuming CPU cycles without advancing. Livelock is therefore a separate failure mode, addressed under Starvation and Livelock, even though both share the symptom of "the system never finishes its work."

Why doesn't preventing deadlock by disallowing hold-and-wait come for free in practice?

Because requiring a process to request all resources at once (or release everything before requesting more) can drastically hurt both correctness ergonomics and efficiency. A process may not know in advance every resource it will need, forcing conservative over-requesting that wastes resources system-wide and reduces concurrency (since resources sit reserved but unused). It can also require restructuring code so operations are atomic at a coarser granularity than natural, complicating error handling. This is exactly the kind of tradeoff explored more fully once you study each concrete strategy under Deadlock Prevention and Avoidance.

Before this

Where this leads