Deadlocks — Cheat Sheet
Operating Systems · 6 topics. Download the PDF or the Instagram carousel and share it.
Deadlock: Four Necessary Conditions
A deadlock occurs only when all four Coffman conditions hold simultaneously: mutual exclusion, hold and wait, no preemption, and circular wait.
- ✓All four Coffman conditions must hold simultaneously; removing any one prevents deadlock.
- ✓Mutual Exclusion is often unavoidable for inherently non-shareable resources like write locks.
- ✓Hold and Wait can be broken by requiring processes to request all resources upfront or release existing ones before requesting new ones.
- ✓Circular Wait is the easiest condition to break in practice: enforce a global total ordering on lock acquisition.
- ✓Java's jstack and ThreadMXBean.findDeadlockedThreads() are the primary tools for diagnosing deadlocks in production.
- ✓A deadlock differs from a livelock (threads active but making no progress) and starvation (one thread indefinitely delayed).
// Classic two-lock deadlock: Thread A locks L1 then L2,
// Thread B locks L2 then L1 — circular wait guaranteed.
Object L1 = new Object();
Object L2 = new Object();
Thread threadA = new Thread(() -> {
synchronized (L1) { // holds L1
System.out.println("A: acquired L1");
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (L2) { // waits for L2
System.out.println("A: acquired L2");
}
}
}, "Thread-A");
Thread threadB = new Thread(() -> {
synchronized (L2) { // holds L2
System.out.println("B: acquired L2");
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (L1) { // waits for L1 → DEADLOCK
System.out.println("B: acquired L1");
}
}
}, "Thread-B");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
// Program hangs — both threads blocked foreverResource Allocation Graph
A Resource Allocation Graph (RAG) is a directed graph used to represent the state of resource allocation and detect deadlocks in single- and multi-instance resource systems.
- ✓In a RAG, request edges go from process to resource; assignment edges go from resource to process.
- ✓For single-instance resources, a cycle in the RAG is both necessary and sufficient for deadlock.
- ✓For multi-instance resources, a cycle is necessary but not sufficient — use the reduction algorithm to confirm deadlock.
- ✓Each dot inside a resource rectangle represents one instance; the number of dots equals total instances.
- ✓The RAG can be converted to a wait-for graph (only processes) by collapsing resource nodes for simpler cycle detection.
import java.util.*;
// RAG represented as adjacency list
// Nodes: "P1","P2","P3" = processes; "R1","R2" = resources (single-instance)
Map<String, List<String>> graph = new HashMap<>();
// Assignment edges (R → P): R1 assigned to P1, R2 assigned to P2
graph.put("R1", List.of("P1"));
graph.put("R2", List.of("P2"));
// Request edges (P → R): P2 wants R1, P3 wants R2, P1 wants R3 (cycle!)
graph.put("P1", List.of("R2")); // P1 holds R1, wants R2
graph.put("P2", List.of("R1")); // P2 holds R2, wants R1 → CYCLE
graph.put("P3", List.of());
// DFS cycle detection
Set<String> visited = new HashSet<>();
Set<String> inStack = new HashSet<>();
boolean hasCycle = false;
for (String node : graph.keySet()) {
if (!visited.contains(node) && dfs(node, graph, visited, inStack)) {
hasCycle = true;
break;
}
}
System.out.println("Deadlock detected: " + hasCycle); // true
static boolean dfs(String node, Map<String, List<String>> graph,
Set<String> visited, Set<String> inStack) {
visited.add(node);
inStack.add(node);
for (String neighbor : graph.getOrDefault(node, List.of())) {
if (!visited.contains(neighbor) && dfs(neighbor, graph, visited, inStack)) return true;
if (inStack.contains(neighbor)) return true; // back edge = cycle
}
inStack.remove(node);
return false;
}Deadlock Prevention
Deadlock prevention eliminates deadlock by ensuring at least one of the four Coffman conditions can never hold in the system.
- ✓Prevention targets one Coffman condition structurally, making deadlock impossible at design time.
- ✓Lock ordering (enforcing total order on acquisition) is the most widely used prevention strategy in Java.
- ✓tryLock() with timeout breaks hold-and-wait but requires careful retry logic to avoid livelock.
- ✓Eliminating mutual exclusion is only possible for inherently shareable resources (e.g., read-only data, ReadWriteLock).
- ✓Prevention is safer than detection but may underutilise resources (e.g., holding all locks upfront blocks other processes).
- ✓Java's ReadWriteLock reduces mutual exclusion by allowing concurrent readers, preventing deadlocks on read-heavy data.
import java.util.concurrent.locks.ReentrantLock;
// Assign a numeric ID to each lock — always acquire lower ID first
ReentrantLock lockA = new ReentrantLock(); // id = 1
ReentrantLock lockB = new ReentrantLock(); // id = 2
// SAFE: both threads acquire in the same order (A before B)
Runnable safeTask = () -> {
lockA.lock();
try {
System.out.println(Thread.currentThread().getName() + ": acquired A");
lockB.lock();
try {
System.out.println(Thread.currentThread().getName() + ": acquired B");
// critical section
} finally {
lockB.unlock();
}
} finally {
lockA.unlock();
}
};
Thread t1 = new Thread(safeTask, "T1");
Thread t2 = new Thread(safeTask, "T2");
t1.start();
t2.start();
// No deadlock: circular wait condition is broken
// T2 will block on lockA.lock() until T1 releases itDeadlock Avoidance: Banker's Algorithm
The Banker's Algorithm dynamically evaluates each resource request to ensure the system stays in a safe state — a state from which all processes can eventually complete.
- ✓A safe state guarantees a safe sequence exists; an unsafe state may or may not lead to deadlock.
- ✓The Banker's Algorithm requires processes to declare maximum resource needs upfront — impractical for general-purpose OS.
- ✓Time complexity of the safety algorithm is O(n² × r) where n is processes and r is resource types.
- ✓The algorithm prevents deadlock dynamically at runtime without imposing permanent structural restrictions.
- ✓Cloud and database admission control systems use Banker's-style reasoning to avoid overcommitting resources.
- ✓A process in an unsafe state is made to wait, not killed — the system may eventually become safe as others release resources.
// Banker's Algorithm — Safety Check
// 3 processes (P0, P1, P2), 3 resource types (A, B, C)
int[] available = {3, 3, 2}; // free instances
int[][] allocation = { // currently allocated
{0, 1, 0}, // P0
{2, 0, 0}, // P1
{3, 0, 2}, // P2
};
int[][] max = { // maximum demand
{7, 5, 3}, // P0
{3, 2, 2}, // P1
{9, 0, 2}, // P2
};
int n = 3, r = 3;
int[][] need = new int[n][r];
for (int i = 0; i < n; i++)
for (int j = 0; j < r; j++)
need[i][j] = max[i][j] - allocation[i][j];
boolean[] finished = new boolean[n];
int[] safeSeq = new int[n];
int count = 0;
int[] work = available.clone();
while (count < n) {
boolean found = false;
for (int i = 0; i < n; i++) {
if (!finished[i]) {
boolean canRun = true;
for (int j = 0; j < r; j++)
if (need[i][j] > work[j]) { canRun = false; break; }
if (canRun) {
for (int j = 0; j < r; j++) work[j] += allocation[i][j];
safeSeq[count++] = i;
finished[i] = true;
found = true;
}
}
}
if (!found) { System.out.println("UNSAFE STATE"); break; }
}
// Safe sequence: P1 → P2 → P0 (need[P1]≤avail, then P2, then P0)Deadlock Detection & Recovery
Deadlock detection algorithms identify when deadlock has occurred, and recovery strategies break the deadlock by terminating processes or preempting resources.
- ✓Detection allows deadlock to occur but identifies and resolves it; it is preferred when deadlock is infrequent.
- ✓Wait-for graph (single-instance) and reduction algorithm (multi-instance) are the two main detection approaches.
- ✓Running detection on every resource request is O(n²) — most systems run it periodically or on CPU utilisation drop.
- ✓Process termination is simple but costly; resource preemption requires rollback support and risks starvation.
- ✓Java's ThreadMXBean.findDeadlockedThreads() detects both monitor locks and java.util.concurrent lock cycles.
- ✓jstack <pid> produces a thread dump; search for "Found one Java-level deadlock" in the output.
import java.lang.management.*;
public class DeadlockDetector {
// Call periodically (e.g., every 30s via ScheduledExecutorService)
public static void checkForDeadlocks() {
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
// findDeadlockedThreads: synchronized + j.u.c locks
// findMonitorDeadlockedThreads: synchronized only
long[] ids = tmx.findDeadlockedThreads();
if (ids == null) {
System.out.println("[OK] No deadlock detected.");
return;
}
ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);
StringBuilder sb = new StringBuilder("DEADLOCK DETECTED!
");
for (ThreadInfo ti : infos) {
sb.append(" Thread: ").append(ti.getThreadName())
.append(" [").append(ti.getThreadState()).append("]
");
sb.append(" Blocked on: ").append(ti.getLockName()).append("
");
sb.append(" Lock held by: ").append(ti.getLockOwnerName()).append("
");
// Print stack trace of deadlocked thread
for (StackTraceElement ste : ti.getStackTrace()) {
sb.append(" at ").append(ste).append("
");
}
}
System.err.println(sb);
// Recovery: alert ops team, restart affected threads, or JVM
}
}
// Schedule detection every 30 seconds
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
DeadlockDetector::checkForDeadlocks, 0, 30, TimeUnit.SECONDS);Livelock & Starvation
Livelock occurs when processes keep changing state in response to each other but make no progress, while starvation occurs when a process is indefinitely denied the resources it needs.
- ✓Livelock threads are active (not blocked) but make no progress — they consume CPU unlike deadlocked threads.
- ✓Randomised backoff with exponential delay (like Ethernet's CSMA/CD) is the standard livelock remedy.
- ✓Starvation occurs when scheduling policy indefinitely postpones a process — common with strict priority scheduling.
- ✓Aging gradually increases a waiting process's priority to guarantee it eventually runs.
- ✓ReentrantLock(true) enables fairness (FIFO ordering) at the cost of ~10-30% throughput reduction.
- ✓Java's ForkJoinPool uses work-stealing: idle threads steal tasks from busy threads, preventing starvation of long-queued tasks.
import java.util.concurrent.atomic.AtomicBoolean;
// LIVELOCK: two "polite" threads each yield endlessly
AtomicBoolean resource = new AtomicBoolean(false); // false = available
Runnable politeThread = () -> {
String name = Thread.currentThread().getName();
int attempts = 0;
while (!resource.compareAndSet(false, true)) {
// Resource busy — politely back off
System.out.println(name + ": resource busy, yielding...");
Thread.yield(); // both threads yield forever — livelock!
attempts++;
if (attempts > 1000) {
System.out.println(name + ": giving up after 1000 attempts (livelock)");
return;
}
}
System.out.println(name + ": acquired resource!");
resource.set(false); // release
};
// FIX: randomised backoff breaks symmetry
Runnable fixedThread = () -> {
String name = Thread.currentThread().getName();
while (!resource.compareAndSet(false, true)) {
try {
// Random sleep: one thread will wait longer than the other
long backoff = (long)(Math.random() * 50); // 0-50ms
System.out.println(name + ": backing off for " + backoff + "ms");
Thread.sleep(backoff);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println(name + ": acquired resource!");
resource.set(false);
};