Thread Starvation and Deadlock

On this page 14

Part II — Concurrency · Interview reference

Liveness failures: the system is “running” but work does not make progress. Seniors must define deadlock vs livelock vs starvation, know Coffman conditions, and design prevention / detection strategies.


Definitions

FailureDefinition
DeadlockSet of threads permanently blocked; each waits for a resource held by another in the set (cycle)
LivelockThreads keep changing state in response to each other but make no useful progress
StarvationA thread never gets resources / CPU because others are continually preferred
Priority inversionLower-priority holds lock needed by higher-priority; medium-priority preempts → high waits (classic RT issue)

Deadlock — Coffman conditions (all required)

  1. Mutual exclusion — resource not shareable
  2. Hold and wait — hold one resource while waiting for another
  3. No preemption — cannot forcibly take resource away
  4. Circular wait — cycle in wait-for graph

Break any one to prevent deadlock.


Wait-for graph mental model

T1 → T2 → T3 → T1   (cycle ⇒ deadlock)

Nodes = threads (or transactions); edges = “waits for resource held by.”

DB systems detect cycles in lock waits; app servers rely on dumps + careful design.


Common deadlock shapes in services

ShapeExample
Lock order inversionT1: lock A then B; T2: lock B then A
Nested synchronizedDifferent call paths acquire monitors in different orders
Thread-pool deadlockAll pool threads blocked waiting for results that need the same pool
Connection pool + lockHold DB connection while waiting on another locked resource
Distributed deadlockA waits on B’s RPC; B waits on A’s RPC (harder to see)

Classic interview scenario: submitting tasks to the same Executor from inside a task and then Future.get() — can deadlock a fixed pool.

Production case study (high volume)

Context: Order orchestration service: fixed pool of 32 workers; each task fans out to inventory + pricing CompletableFutures submitted to the same pool, then join(). Why seniors care: Pool-induced deadlock takes down checkout for everyone; looks like “hang” with healthy CPU; blast radius is the whole dependency graph sharing that executor. Failure / symptom: All threads WAITING on Future.get; queue depth grows; LB health fails; jstack shows classic cycle. Resolution: Separate pools for orchestration vs blocking work; supplyAsync on dedicated executor; avoid blocking worker on same-pool children; deadlock detection via ThreadMXBean in canaries. Seen at / similar to: Widespread Spring @Async misuse; Cassandra Java driver / Netty event-loop deadlocks as cousins; Amazon Builders’ Library “avoiding deadlock” themes.


Prevention strategies

StrategyHow
Lock orderingGlobal total order; always acquire by order
Lock timeout / tryLockFail + retry / abort instead of infinite wait
Avoid hold-and-waitAcquire all needed locks at once; or don’t hold while calling out
No locks / finer designImmutability, concurrent structures, single-threaded ownership
Resource hierarchyDocument layers (e.g. never call into module B while holding module A’s lock)
Separate poolsIsolate “orchestration” threads from “blocking work” threads

Detection & recovery

Detection

  • Thread dumps (jstack, IDE, kill -3): look for BLOCKED cycles
  • JFR / lock profilers
  • DB: pg_locks, InnoDB status, deadlock logs
  • Timeouts as soft detection

Recovery

  • Abort one participant (DB does this); app may fail request and retry idempotently
  • Restart process (last resort; masks design bug)

Starvation

Causes

  • Unfair locks under continuous contention (writer starvation with some RW locks)
  • Scheduler / priority misuse
  • Tasks always jumping queue (poor queue policy)
  • Shared pool dominated by long CPU tasks → short tasks starve

Mitigations

  • Fair locks when necessary (throughput cost)
  • Separate queues / pools by workload class
  • Aging / priority boost
  • Bound retries and backoff (also helps livelock)

Livelock

Example: two threads continually yielding to each other; or endless retry storms without jitter.

Mitigations: randomized backoff, retry budgets, circuit breakers, idempotent reconcile loops with progress metrics.

Production case study (high volume)

Context: Multi-region banking ledger: service A waits on B’s RPC while holding a DB row lock; B calls back into A for enrichment (distributed deadlock). Separately, unfair ReadWriteLock starves writers refreshing FX rates. Why seniors care: Local Coffman thinking extends to RPC+DB; livelock from retry storms burns error budgets; writer starvation makes “eventually consistent” configs never converge. Failure / symptom: Lock wait timeouts in Postgres; paired services with elevated latency; FX rates stale for minutes under read floods. Resolution: Never hold DB locks across RPC; break cycles with async events/outbox; timeouts + idempotent retry with jitter; fair locks or separate read snapshots for config; dependency graphs in architecture review. Seen at / similar to: Stripe/bank core ledgers; Google Chubby/lock service lore; AWS “timeouts, retries, and backoff with jitter” classic.


Java under the hood — detection & locks

Tool / APIRole
jstack / jcmd Thread.print / kill -3Thread dump: BLOCKED on monitors, lock owners
ThreadMXBean.findDeadlockedThreads()Programmatic cycle detection (monitors + some ownable synchronizers)
JFR / async-profilerLock contention profiles
AQS fair vs unfairFair reduces starvation, lowers throughput; default locks often unfair
tryLock(timeout)Break hold-and-wait / detect stuck acquisition
Same-pool Future.get()Fixed ThreadPoolExecutor: all workers blocked waiting for tasks that need the same pool → deadlock

Classic fix patterns: run nested work on caller thread, use a separate pool, or async composition without blocking the worker.


What interviewers probe

  1. Define deadlock and give Coffman conditions.
  2. Draw a cycle for a two-lock example; fix with ordering.
  3. Thread-pool deadlock with Future.get() — how to redesign (same-thread execution, separate pool, async composition).
  4. How do you diagnose in production? (dumps, metrics: stuck threads, queue latency).
  5. Starvation vs deadlock differences.
  6. Distributed systems: RPC cycles / dependency cycles analogous to deadlock.
  7. Production dumps — narrate finding a same-pool Future.get deadlock under peak traffic.

Senior-level expectation: Prevention by design; recognition of pool-induced deadlocks; operational diagnosis path.


Pitfalls

  • Logging + locking in inconsistent order across modules
  • Calling foreign/unknown code while holding locks (callbacks!)
  • One giant shared executor for mixed blocking workloads
  • Infinite retries without jitter → livelock-like outages
  • “Fixing” deadlock only with larger timeouts (hides, adds latency)
  • Holding row locks while calling sister services

Cross-references

Interview reference — explanation quality and judgment, not syntax memorization.