skip to content

Race Conditions and Deadlocks

The failure modes concurrency invites: data races on shared state, deadlock and its four Coffman conditions, livelock and starvation, plus detection and the lock-ordering discipline that prevents them. Interviewers favour this because the bugs are intermittent, load-dependent, and famously hard to reproduce.

part ofComputer science fundamentalsoverview, primer and where to startread it →
on this pageshow

questions

23

Two threads each run the statement `counter = counter + 1` one thousand times on a shared variable, with no synchronization. The final value is often less than 2000. Explain why, using what the hardware actually executes.

level: juniorimportance: must knowfreq 72%

answer

  1. one statement, three steps: load, add, store
  2. both read 5, both write 6 → lost update
  3. atomic ⇔ nobody sees the middle
  4. compound action: read-modify-write, check-then-act
  5. fix: lock, CAS, or don't share

basics

~20 s

That one statement is three machine steps: read the value, add one, write it back. Two threads can read the same old value, both add one to it, and both write the same new value — so two increments produce one. Updates are lost whenever the steps interleave.

solid answer

~50 s

`counter = counter + 1` looks atomic in source but is a **read-modify-write**: load the current value, compute value+1, store the result. Nothing stops another thread from running between the load and the store. Interleave two threads starting at 5: both load 5, both compute 6, both store 6. Two increments happened; the counter advanced by one. That is a **lost update**, and with a thousand iterations each you lose an unpredictable number of them, so the result is anything between 1000 and 2000. The general rule: an operation is atomic only if no other thread can observe or interfere with its intermediate state. A sequence of individually-safe steps is not automatically a safe sequence — that is a **compound action**, and it needs to be made atomic explicitly: hold a lock across the whole read-modify-write, use a hardware atomic increment or compare-and-swap, or give each thread its own counter and combine at the end.

code

text · 7 lines
text
counter starts at 5

  time ->
  A: load(5) .... add(6) .......... store(6)
  B: ..... load(5) .... add(6) ............ store(6)

  two increments performed, counter == 6

go deeper

for a junior

Show the three steps and walk one concrete interleaving where both threads read the same value; name it a lost update and give one fix.

for a middle

Generalize to compound actions — read-modify-write and check-then-act — and compare the fixes: lock, atomic/CAS, per-thread accumulation, immutability.

for a senior

Stress that atomicity of individual accesses is a different property from atomicity of the compound action, and that the right question is where the atomic boundary must sit.

for a principal

Frame it as invariant design: which state must change together, whether the counter should be shared at all, and when to prefer per-thread reduction or single-owner state over synchronizing a hot shared location.

## What the statement really is Source code hides how much work a statement is. `counter = counter + 1` compiles to roughly three operations: 1. **load** — copy the current value of `counter` from memory into a register; 2. **add** — compute register + 1; 3. **store** — write the register back to `counter`. A thread can be preempted between any two of those, and on a multicore machine two threads execute genuinely simultaneously anyway. There is no rule that says these three steps happen as a unit — that property is called **atomicity**, and you only get it if something provides it. ## The interleaving that loses an update Start with counter = 5. ``` Thread A Thread B counter load -> 5 5 load -> 5 5 add -> 6 5 add -> 6 5 store 6 6 store 6 6 ``` Both threads did their job correctly. Both incremented "the value they read". But B's read happened before A's write, so B computed from a stale value and overwrote A's result. Two increments, one net change: a **lost update**. With a thousand iterations per thread, this happens an unpredictable number of times, which is why the total is some non-deterministic value ≤ 2000. It is also why the bug is so treacherous: the window is nanoseconds wide, so on a lightly loaded developer machine the count is often exactly right, and it only misbehaves on the production machine with more cores and more contention. ## The general shape: compound actions Increment is one instance of a family. A **compound action** is a sequence of operations that must appear indivisible to be correct. Two forms cover almost all of them: - **Read-modify-write** — read a value, derive a new one from it, write it back. Increment, append, "add this amount to the balance", "set flag if larger". - **Check-then-act** — test a condition, then act on the assumption that the condition still holds. "If the key is absent, insert it." "If the file does not exist, create it." "If balance ≥ amount, withdraw." In both, the danger is the **gap**: between the read (or check) and the write (or act), another thread can change the thing you based your decision on. Your decision is then based on a fact that is no longer true, and you act on stale information. ## Why "it's just one line" is not a defense The atomicity of an operation has nothing to do with how it looks in source. Whole-statement atomicity is not something languages generally promise for arbitrary expressions on shared variables. Even a single machine instruction is not automatically atomic across cores unless it is specified to be — that is exactly what dedicated atomic instructions (atomic add, compare-and-swap) exist to provide. Note also that making the variable's individual reads and writes atomic (so no torn or half-written value is ever observed) does **not** fix this bug. Every read and every write above was clean; the problem is that the *pair* was not a unit. Visibility/atomicity of single accesses and atomicity of a compound action are different properties, and increment needs the second. ## The fixes, and what each one costs 1. **Mutual exclusion.** Acquire a lock before the load and release it after the store. Correct and general — it works for arbitrarily complex compound actions, including ones spanning several variables. Costs contention when many threads hit the same lock. 2. **A hardware atomic operation.** Atomic increment, or a compare-and-swap retry loop: read the value, compute the new one, and swap it in *only if* the variable still holds the value you read; if not, retry. This closes the gap by making the check and the write one indivisible step. Fast and lock-free, but only applies to a single memory location. 3. **Don't share.** Give each thread its own counter and sum them at the end (per-thread accumulation / reduction), or confine the counter to a single owning thread and send it messages. No contention at all; costs a combining step and some memory. 4. **Immutability.** If a value never changes after publication, no read-modify-write exists to race. ## The interview-ready summary Ask of any shared-state operation: *is there a moment between my read and my write where another thread could change the value I based the write on?* If yes, the operation is a compound action, it is not atomic just because it is one statement, and you must widen the atomic unit — with a lock, with a hardware atomic, or by not sharing the state at all.

  • If reads and writes of the counter were guaranteed atomic and immediately visible to all threads, would the count now be correct?
    No. Every individual read and write in the losing interleaving was already clean and well-formed; nothing was torn or stale in the hardware sense. The bug is that the read and the write are not a single unit, so another thread can slip in between them. Fixing visibility fixes a different problem — it stops threads reading indefinitely stale values — but leaves the lost update intact.
  • Give another everyday example of the same bug that is not a counter, and say what makes it the same.
    "If the key is absent, insert it" over a shared map: two threads both find the key absent and both insert, so one overwrites the other or a duplicate is created. Likewise "if balance ≥ amount, subtract amount" lets two withdrawals both pass the check and overdraw the account. In each case a decision is made from a read value and acted on later, and the state can change in the gap — check-then-act, the sibling of read-modify-write.

Two people update the same paper tally by reading the number, writing it on a sticky note, adding one, and copying it back. If both read '5' before either writes, the pad ends up at 6 no matter how carefully each of them did their arithmetic.

saying these in an interview costs you the question

  • 'It's a single statement so it's atomic'
  • 'It only loses updates if the OS preempts the thread' — on multiple cores they run simultaneously with no preemption needed
  • 'Making the variable's reads and writes atomic/visible fixes the count'
  • 'It worked on my machine a hundred times, so it's fine' — the window is nanoseconds and load-dependent
  • 'Just make the loop faster / add a small sleep' as a fix

context

open as a page

What is a deadlock, and which four conditions must all hold at the same time for one to be possible?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Deadlock is a set of threads permanently blocked, each waiting for a resource another one in the set holds. It requires four conditions at once: mutual exclusion, hold-and-wait, no preemption, circular wait.

open as a page

Interviewers often distinguish a 'data race' from a 'race condition'. Define each precisely, and show that neither one implies the other.

level: middleimportance: must knowfreq 62%

basics

~20 s

A data race is a memory-model property: two threads access the same location concurrently, at least one writes, with no ordering between them. A race condition is a correctness property: the result depends on timing. You can have a race condition with zero data races (all accesses properly synchronized, but the logic assumes state cannot change between steps) and a data race that never produces a wrong-looking answer.

open as a page

Two threads transfer money between accounts, each locking the source account and then the destination. Explain why this can hang forever, how a global lock ordering fixes it, and what you do when the objects being locked have no natural order.

level: middleimportance: must knowfreq 65%

basics

~20 s

Transfer(A,B) and transfer(B,A) take the two locks in opposite orders and can each hold one while waiting for the other. Fix: define one total order over all lockable objects, such as a unique id, and always acquire in that order.

open as a page

What is livelock, and how does it differ from deadlock? Describe the runtime signature of each.

level: middleimportance: must knowfreq 62%

basics

~20 s

Both are liveness failures: no useful work completes. In deadlock the threads are blocked forever waiting on each other, so their states are frozen. In livelock the threads stay runnable and keep changing state (retry, back off, yield) but the pattern repeats and nobody finishes.

open as a page

Three tasks with different priorities run on one processor and share a resource. Walk through how the lowest-priority task can block the highest-priority one for an unbounded time, and explain what role the middle-priority task plays.

level: middleimportance: must knowfreq 45%

basics

~20 s

Low takes a lock, then High preempts and blocks on that lock. Now Low must finish to release it — but Medium, needing no lock, preempts Low because it outranks it. High waits on Low, Low waits for CPU behind Medium, so High is effectively blocked by Medium for as long as Medium runs.

open as a page

A service stops serving requests, the process is alive, and CPU is near zero. Working only from stack snapshots of every thread, how do you confirm it is a deadlock and identify which threads form the cycle?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Take several snapshots seconds apart. Deadlocked threads are blocked, not running, and their stacks are identical across snapshots. For each, read what it waits on and what it already holds, build the wait-for graph, and look for a cycle.

open as a page

Several clients each retry a failed operation after the same fixed delay and keep colliding on every attempt, so almost none succeed. Explain what failure this is and why adding randomized jitter to the retry delay helps more than simply lengthening the delay.

level: seniorimportance: must knowfreq 55%

basics

~20 s

It is a symmetric retry storm — a livelock. Identical delays keep the clients synchronised, so they re-collide every round no matter how long the delay. Jitter spreads their wake-up times, so one wins each round and the group de-synchronises instead of marching in lockstep.

open as a page

How would you model a set of blocked threads as a graph to prove they are deadlocked, and when is finding a cycle in that graph not enough to conclude deadlock?

level: middleimportance: should knowfreq 44%

basics

~20 s

Build a wait-for graph: one node per thread, an edge from waiter to holder. A cycle means deadlock when each resource has one instance. With multi-instance resources or wait-for-any requests, a cycle is necessary but not sufficient.

open as a page

What does it mean for a thread to be starved, and which properties of a lock implementation or a scheduler make starvation possible? Give at least two concrete mechanisms.

level: middleimportance: should knowfreq 46%

basics

~20 s

Starvation is when a runnable thread never gets the resource it needs, even though the system as a whole keeps making progress. It comes from unbounded overtaking: barging (non-queued) locks, LIFO or priority-ordered wait sets, strict priority scheduling with a busy high-priority class, and readers that keep a writer out.

open as a page

A developer argues that an unsynchronized shared statistics counter is a 'benign' race because a slightly wrong count is acceptable. Explain, in terms of what a memory model actually guarantees, why that reasoning is unsound and what outcomes are genuinely possible.

level: seniorimportance: should knowfreq 29%

basics

~20 s

Memory models define behaviour only for data-race-free programs. Once a race exists, the compiler and hardware were allowed to optimize under the assumption it could not happen, so you do not get 'the right answer, occasionally off by a few' — you can get a value never written, a loop that never terminates, a torn value, or code paths that make no sense in the source.

open as a page

A program checks a condition about a resource and then acts on the result of that check — for example verifying a file path is permitted and then opening it. Explain the time-of-check-to-time-of-use flaw this creates and how you eliminate it.

level: seniorimportance: should knowfreq 38%

basics

~30 s

Between the check and the use, whatever the check examined can change — a path can be swapped for a symlink, a record can be deleted, permission can be revoked. The verified fact no longer holds when you act on it. The fix is to remove the gap: perform the check and the action as one atomic operation, or check the handle you actually operate on rather than the name you looked up.

open as a page

Give an example of a deadlock in which no participant holds a mutex, and explain what plays the role of the lock in it.

level: seniorimportance: should knowfreq 38%

basics

~20 s

A task holding one pooled connection while needing a second one, in a pool where every connection is held by such a task. The pool permit is the lock: finite, exclusive, non-preemptible. Bounded queues, semaphores and worker slots behave the same.

open as a page

What does the rule 'never invoke code you do not control while holding a lock' mean, and why is shrinking a critical section a weaker argument for deadlock safety than it first appears?

level: seniorimportance: should knowfreq 45%

basics

~20 s

An open call means releasing your lock before calling callbacks, plugins or remote services, because their internal locks add wait edges you cannot order. Shrinking a critical section only narrows the timing window; it lowers probability, not possibility.

open as a page

Describe the strategy of attempting each lock acquisition with a timeout and releasing everything already held on failure, and explain what must be added so the retry loop does not spin forever.

level: seniorimportance: should knowfreq 42%

basics

~20 s

Acquire the first lock, then attempt the next with a timeout; if it fails, release everything held, wait a randomized interval, and retry the whole operation. It needs randomized backoff, a retry cap, and rollback-safe partial work.

open as a page

A mutual-exclusion lock can hand ownership to the longest-waiting thread in strict FIFO order, or let whichever thread happens to be running acquire it first. Compare the two policies and explain why the fair one usually delivers lower throughput.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Fair/FIFO locks guarantee bounded waiting, so no thread starves, but every handoff waits for the next waiter to be rescheduled and runs with cold caches. Unfair/barging locks let a running thread take the lock immediately — far higher throughput, at the cost of an unbounded tail and possible starvation.

open as a page

Compare priority inheritance with priority ceiling protocols as remedies for a high-priority task being delayed by a lower-priority lock holder. When would you choose each, and what does each cost?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Inheritance is reactive: when a high task blocks, the holder temporarily inherits its priority, then reverts. Ceiling is proactive: each resource carries the highest priority of any task that may use it, and a task is raised to that ceiling on acquisition. Ceiling needs static knowledge but bounds blocking to one section and prevents deadlock.

open as a page

Every shared data structure in a service is individually thread-safe, yet operations still produce inconsistent results under load. Explain why per-object thread safety is not enough, and how you decide where the atomicity boundary belongs in a design.

level: principalimportance: should knowfreq 40%

basics

~20 s

Thread-safe components make each individual operation atomic, but invariants usually span several operations or several objects. Those sequences are compound actions and stay racy. The atomicity boundary must be drawn around the invariant — every piece of state that must change together needs one owner, one lock, one transaction, or one conditional operation.

open as a page

A component keeps producing deadlocks because its operations acquire several locks. What redesigns remove the possibility entirely rather than managing it, and what does each one cost?

level: principalimportance: should knowfreq 36%

basics

~20 s

Collapse to one coarse lock; partition state so each operation touches one shard; make data immutable or copy-on-write; give state a single owner and use messages; or use lock-free atomic updates. Each trades throughput, memory, latency or complexity for the guarantee.

open as a page

Priority inversion is usually taught with real-time operating systems. Where does the same failure shape appear in ordinary server or cloud systems, and how would you design a latency-critical path so it cannot be delayed by lower-importance work holding a shared resource?

level: principalimportance: should knowfreq 32%

basics

~20 s

Anywhere an urgent request waits on an exclusive resource held by work that can be starved: a lock or database row held by a CPU-throttled container, a descheduled vCPU, a background job in a priority queue. Fix structurally — do not share the resource across importance classes, keep holders un-throttleable, and cap hold time.

open as a page

What is the difference between deadlock prevention and deadlock avoidance, and how does the banker's algorithm decide whether to grant a resource request?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Prevention removes one of the four necessary conditions structurally, so deadlock cannot arise. Avoidance allows all four but refuses any request that would leave an unsafe state. The banker's algorithm grants a request only if a safe completion sequence still exists.

open as a page

The 1997 Mars Pathfinder lander repeatedly reset itself on the Martian surface, triggered by a watchdog timer. The root cause was a concurrency scheduling defect. Describe what went wrong, how it was fixed remotely, and what engineering lessons it carries.

level: middleimportance: nice to knowfreq 30%

basics

~20 s

A high-priority bus-management task waited on a mutex held by a low-priority meteorological task, which medium-priority communications work kept off the CPU — unbounded priority inversion. A watchdog saw the bus task miss its deadline and reset the system. The fix was to enable priority inheritance on that mutex, uploaded from Earth.

open as a page

You operate a shared service where many tenants contend for one bounded pool of workers. How do you decide how much fairness to enforce between them, and what would you measure to know the choice was right?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Start from the guarantee you owe each tenant, not from the mechanism. Enforce the weakest policy that bounds the worst case: per-tenant concurrency caps or weighted fair queueing plus admission control, unfair-fast inside a tenant. Measure per-tenant p99/max wait and age-of-oldest, not aggregate throughput.

open as a page