skip to content

Memory Models and Atomics

What a thread is actually allowed to see of another thread's writes: cache coherence, acquire/release and sequential consistency, happens-before edges, volatile, and compare-and-swap as the basis of lock-free code. Interviewers use it to separate people who know why the code works from people who added volatile until it did.

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

questions

29

Two threads each run the statement `count = count + 1` a thousand times on the same shared variable, and the final total is less than two thousand. Explain why, and what it means for an operation to be an atomic read-modify-write.

level: juniorimportance: must knowfreq 78%

answer

  1. one line = load, add, store
  2. both read 41, both write 42 = lost update
  3. preemption makes it happen on one core too
  4. RMW = read+modify+write indivisibly
  5. visibility fixes staleness, not the gap

basics

~20 s

The statement is three steps: read, add one, write back. Two threads can read the same old value and both write the same new value, so one increment is lost. An atomic read-modify-write performs all three steps as one indivisible hardware operation that no other thread can interleave with.

solid answer

~50 s

`count = count + 1` is not one operation. It compiles to a load, an add, and a store. Any interleaving is possible between them, so two threads can both load 41, both compute 42, and both store 42 — one increment vanishes. This is a lost update, and it happens even on a single core, because a thread can be preempted between the load and the store. An **atomic read-modify-write** is a single hardware operation that reads a memory location, computes a new value from it, and writes it back with no other thread able to observe or intervene at an intermediate point. Fetch-and-add, exchange, and compare-and-swap are the usual primitives. The processor achieves this by taking exclusive ownership of the cache line for the duration. Note that making the variable merely *visible* across threads is not enough. Visibility fixes staleness; it does nothing for the read-then-write gap. You need atomicity of the whole trio, which means an atomic operation or a lock.

code

text · 7 lines
text
NON-ATOMIC                      ATOMIC
count = 41                      count = 41
T1 load  41                     T1 fetch_add(count,1) -> 41
T2 load  41                     T2 fetch_add(count,1) -> 42
T1 store 42                     count = 43
T2 store 42
count = 42   (one lost)

go deeper

for a junior

Say the three steps out loud and walk the two-thread interleaving that loses an increment; name atomic increment or a lock as the fix.

for a middle

Add why visibility alone is insufficient, and name the concrete primitives (fetch-and-add, exchange, compare-and-swap).

for a senior

Point out that atomicity per operation does not compose into atomicity of a sequence, and mention the cache-line ownership cost of read-modify-write instructions.

for a principal

Question whether the counter should be shared at all: per-thread accumulation, sharding, or approximate counting often beat making the hot path atomic.

## Why one line is three operations Processors do not compute on memory; they compute in registers. An increment therefore always becomes: ``` load r <- count # read add r <- r + 1 # modify store count <- r # write ``` Each step is individually indivisible, but the sequence is not. Between any two steps, another thread on another core can execute freely, and even on one core the scheduler can preempt between them. ``` count = 41 T1: load r1 = 41 T2: load r2 = 41 T1: r1 = 42 ; store count = 42 T2: r2 = 42 ; store count = 42 count = 42 (two increments applied, one survived) ``` Nothing was corrupted at the bit level — each store wrote a legal value. What was lost is the *dependency*: T2's write should have been based on T1's result and was not. This class of bug is called a **lost update**, and it is the simplest possible data race. A key detail candidates get wrong: this is not merely a multicore phenomenon. Preemption on a single core creates the same interleaving. "We only run one core" is not a defence; neither is "the window is tiny" — it just makes the loss rare and load-dependent, which is worse for diagnosis. ## What atomic read-modify-write means An operation is **atomic** with respect to other threads if no thread can observe it half-done and no thread's operation can interleave inside it. A **read-modify-write (RMW)** atomic does all three phases as one unit: - **fetch-and-add(loc, delta)** — adds and returns the previous value. - **exchange(loc, v)** — stores v and returns the previous value. - **compare-and-swap(loc, expected, new)** — stores new only if the location currently equals expected; reports whether it did. - **fetch-and-or / fetch-and-and** and similar bitwise forms. With fetch-and-add, the earlier trace becomes impossible: the hardware serialises the two operations, so one returns 41 and the other returns 42, and the final value is 43. Every RMW has a single, well-defined position in a total order of operations on that location. ## How hardware provides it On cache-coherent machines, the core executing an RMW acquires the target cache line in an exclusive state and holds it for the duration of the operation, so no other core can read or write it in between. Alternatively the architecture provides load-linked/store-conditional, where the store fails if the line was touched since the load, and the operation is retried. Either way the cost is real: an RMW forces exclusive ownership of a line, which is far more expensive than a plain load and gets worse as more cores contend for the same line. ## Atomicity is not visibility, and not compound safety Three distinctions worth stating explicitly: **Atomicity versus visibility.** Marking a variable as "always read from memory" or otherwise ensuring writes become visible to other threads solves staleness — it does not close the read-then-write gap, because two threads can still both read the freshest value and both write the same result. Increment needs atomicity, not just visibility. **Atomicity of one operation versus of a sequence.** An atomic increment protects one increment. It does not protect `if (count < LIMIT) count = count + 1`, because the check and the update are two atomic operations with a gap between them. Compound invariants need either a compare-and-swap loop that re-validates, or a lock. **Atomicity versus ordering.** An atomic RMW guarantees indivisibility of that operation on that location. Whether surrounding ordinary reads and writes are seen in program order by other threads is governed by the memory ordering the operation carries — a related but separate concern. ## The other correct answer: don't share The cheapest atomic operation is the one you never perform. If each thread keeps a private counter and the totals are summed at the end, there is no shared mutable state, no contention, and no correctness question. Sharing a hot counter should be a deliberate choice, not a default.

  • If the shared variable is declared so that every read sees the latest written value, is the increment now safe?
    No. Guaranteeing freshness fixes visibility, not atomicity. Two threads can both read the newest value, both add one, and both store the same result, losing an update exactly as before. Only an atomic read-modify-write, or a lock held across the read and the write, makes the increment safe.
  • An atomic increment is safe. Is `if (count < limit) atomicIncrement(count)` also safe?
    No. The comparison and the increment are two separate atomic operations, and another thread can push the counter to the limit in the gap between them, so the counter can exceed it. Correct approaches are a compare-and-swap loop that re-reads the value and re-checks the condition before installing the new one, or holding a lock across the whole check-and-act sequence.

Two people update the same total on a whiteboard. Both read 41, both walk away and compute 42, both write 42. Nobody misread anything; the second write was just based on a stale reading.

saying these in an interview costs you the question

  • Believing a single source line executes as one indivisible step
  • Claiming the bug cannot occur on a single-core machine
  • Thinking a visibility guarantee alone makes increment safe
  • Assuming that if every individual operation is atomic, any sequence of them is atomic
  • Dismissing it because 'the window is too small to matter'

context

open as a page

A worker thread spins in a loop reading an ordinary boolean 'stop' variable while another thread sets it to true and then exits. Sometimes the worker never leaves the loop. Explain how that is possible even though the write definitely executed.

level: juniorimportance: must knowfreq 58%

basics

~20 s

Nothing orders the write against the read, so the reader has no obligation to observe it. The compiler may load the variable once into a register and loop on that copy, and the write may sit in the writer's store buffer. Publish the flag through a synchronization edge.

open as a page

In a lock-free algorithm built on compare-and-swap, a thread reads a shared pointer whose value is A, does some work, and its later compare-and-swap from A succeeds — yet the structure ends up corrupted. Explain what went wrong and why a successful compare-and-swap was not enough.

level: middleimportance: must knowfreq 45%

basics

~20 s

That is the ABA problem. Between the read and the compare-and-swap, other threads changed the value to B and back to A. Compare-and-swap only checks that the word is equal now, not that nothing happened meanwhile, so anything the thread inferred from its first read may be stale.

open as a page

Describe how a compare-and-swap instruction works and how you build an arbitrary atomic update on top of it with a retry loop. Why can the loop iterate more than once, and what happens to a thread that keeps losing?

level: middleimportance: must knowfreq 62%

basics

~20 s

Compare-and-swap atomically writes a new value only if the location still equals the value you expected, reporting success or failure. You read, compute a new value, attempt the swap, and loop on failure with a fresh read. A loop iterates when another thread updated the location first; a persistently unlucky thread can starve, though the system as a whole always progresses.

open as a page

Two threads each increment their own private counter, but the two counters are adjacent fields of the same object. Measured throughput is worse than one thread doing both increments, and it gets worse as you add cores. Explain what the hardware is doing and how you would confirm and fix it.

level: middleimportance: must knowfreq 58%

basics

~20 s

False sharing. Coherence tracks whole cache lines, so both counters live in one line; each write must take exclusive ownership and invalidates the other core's copy, so the line ping-pongs between caches. Confirm with coherence-miss counters or by separating the fields; fix by padding or aligning them onto different lines, or by accumulating per thread.

open as a page

In a shared-memory concurrent program, what does it mean to say that one action happens-before another, and what does that relationship guarantee about what a thread can see?

level: middleimportance: must knowfreq 55%

basics

~20 s

Happens-before orders actions: program order inside a thread, plus synchronization edges across threads (a release paired with a matching acquire). If A happens-before B, every write made before A is visible to B. It is transitive, not chronological.

open as a page

Several languages offer a qualifier that marks a shared variable so its reads and writes participate in the memory model - Java's and C#'s volatile, or a C++ atomic used with release/acquire ordering. What does such a marking guarantee, what does it not guarantee, and how does C's volatile differ?

level: middleimportance: must knowfreq 48%

basics

~20 s

It guarantees the variable is really read and written each time, that a write becomes visible to a later read in finite time, and that the write acts as a release and the read as an acquire, so earlier writes are visible too. It does not make read-modify-write atomic. C's volatile gives none of this.

open as a page

Concurrent algorithms are classified by their progress guarantee: blocking, obstruction-free, lock-free and wait-free. Define each of these terms precisely and explain what practical difference the guarantee makes when a thread is preempted, paused or delayed.

level: middleimportance: must knowfreq 50%

basics

~20 s

Blocking: one stalled thread can stop everyone. Obstruction-free: a thread completes if it eventually runs without interference. Lock-free: some thread always completes in a bounded number of system steps, so the system as a whole progresses. Wait-free: every thread completes within a bounded number of its own steps.

open as a page

Why are compilers and CPUs allowed to execute memory reads and writes in an order different from the source code, and what rule limits how far they can go?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both only promise that a single thread running alone produces the same result — the as-if-serial rule. Within that they hoist, sink, cache values in registers, buffer stores and execute out of order. Another thread watching memory can observe a different order.

open as a page

Compare acquire/release ordering, sequentially consistent ordering, and relaxed ordering for atomic operations: what does each guarantee, what does each cost, and when would you choose the weaker ones?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Relaxed guarantees atomicity only — no ordering with anything else. Acquire/release is a one-way pairing: a release store publishes everything written before it to any thread doing a matching acquire load. Sequential consistency adds a single total order all threads agree on, requiring the expensive full fence.

open as a page

CPU caches do not move memory one byte at a time; they transfer and track it in fixed-size aligned blocks called cache lines (commonly 64 bytes on mainstream hardware). What is a cache line, and what consequences does that granularity have for how a program performs?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A cache line is the fixed-size block of memory (often 64 bytes) a cache loads, owns and invalidates as one unit. So neighbouring data travels together: sequential access is nearly free, scattered access wastes most of each transfer, and unrelated variables sharing a line interfere.

open as a page

How does attaching a version counter to a shared pointer — a stamped or tagged reference updated by a single wide compare-and-swap — prevent the ABA problem, and what are the limits of that technique?

level: middleimportance: should knowfreq 32%

basics

~20 s

You compare-and-swap the pointer and a counter as one unit, bumping the counter on every update. A value that leaves and returns comes back with a different stamp, so the compare-and-swap fails and the thread retries. Limits: it needs a wide atomic, the counter can wrap, and it does not make freeing memory safe.

open as a page

Multi-core CPUs keep a private cache per core yet still present a single view of each memory location. Explain how a MESI-style cache coherence protocol achieves that: what the Modified, Exclusive, Shared and Invalid states mean, and what happens when one core writes a line another core is caching.

level: middleimportance: should knowfreq 38%

basics

~20 s

Each cached line carries a state: Modified (dirty, mine alone), Exclusive (clean, mine alone), Shared (clean, others may hold it), Invalid (unusable). Writing requires exclusive ownership, so the writer requests the line and invalidates all other copies; those drop to Invalid and must refetch, taking the data from the writer's cache.

open as a page

Describe how a concurrent LIFO stack can be built using nothing but an atomic compare-and-set on a single head pointer — the design usually called the Treiber stack. Walk through push and pop, state the progress guarantee it provides, and say where it performs badly.

level: middleimportance: should knowfreq 38%

basics

~20 s

Nodes form a singly linked list from a head pointer. Push: read head, point the new node at it, compare-and-set head from the observed value to the new node, retry on failure. Pop: read head, compare-and-set head to head.next, return its value. It is lock-free but not wait-free, and all traffic funnels through one contended pointer.

open as a page

Two threads run concurrently on shared variables x and y, both initially 0. Thread 1 does: store x=1, then load y into r1. Thread 2 does: store y=1, then load x into r2. On typical hardware, can the program end with r1=0 and r2=0, and why?

level: middleimportance: should knowfreq 40%

basics

~20 s

Yes. Each core buffers its store and lets the later load execute before that store is visible to the other core. No interleaving of the source order explains r1=r2=0, so it proves the machine is not sequentially consistent. Only a full fence between store and load forbids it.

open as a page

In a lock-free data structure written without garbage collection, why is it unsafe for the thread that unlinks a node to free that node immediately, and what problem must any safe-memory-reclamation scheme solve?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Other threads may still hold pointers they read before the unlink, since lock-free readers never announce themselves by taking a lock. Freeing at once causes use-after-free and enables address reuse, which brings back ABA. Reclamation schemes must determine when no thread can still reference a removed node, then free it.

open as a page

Does running on a garbage-collected runtime eliminate the ABA problem in compare-and-swap-based code? Explain precisely what automatic memory management removes and what it leaves behind.

level: seniorimportance: should knowfreq 30%

basics

~20 s

No. A tracing collector removes the use-after-free half and makes address recurrence far less likely, because a node a thread still references is never collected or reused. But if the program itself recycles objects — pools, interning, caches, or reinserting the same object — the value can still recur and ABA returns.

open as a page

Under heavy contention, a shared counter updated with an atomic fetch-and-add instruction often achieves much higher throughput than an equivalent compare-and-swap retry loop that computes the same result. Explain why.

level: seniorimportance: should knowfreq 34%

basics

~20 s

Fetch-and-add cannot fail: every attempt completes, so N threads need N operations. A compare-and-swap loop discards work whenever another thread wins, so attempts grow superlinearly with contention, and each wasted attempt still costs an exclusive cache-line transfer. Fetch-and-add is also wait-free rather than merely lock-free.

open as a page

A single shared atomic counter incremented on every request becomes a throughput bottleneck on a many-core server, and adding cores makes it worse rather than better. Explain the mechanism and describe the techniques that relieve it, with their costs.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Every atomic update needs exclusive ownership of one cache line, so all cores serialise on transferring it and the line ping-pongs. Fixes: split the counter into per-thread or striped cells summed only on read; batch updates locally; back off on retry; or sample instead of counting exactly. Costs are memory, read-time summation, and loss of an exact instantaneous value.

open as a page

In a shared-memory multiprocessor, a value that thousands of threads only read costs almost nothing to access, while a single value that many cores write becomes a throughput ceiling even when the program uses no locks at all. Explain the asymmetry, and describe the techniques you would use to remove such a hot spot.

level: seniorimportance: should knowfreq 42%

basics

~20 s

Clean lines can be replicated in every cache, so reads hit locally and scale. Writing requires exclusive ownership of the line, so every writer invalidates all other copies and the line migrates core to core — a serialisation point. Remove it by sharding state per core, batching updates, or making the hot data read-mostly.

open as a page

Handing another thread a reference to an object you just finished building is a classic hazard in shared-memory concurrency. Why is that hazard a property of the shared-memory model rather than of the object, and how do message-passing designs — actor mailboxes, CSP-style channels, ownership transfer — remove it by construction?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Shared memory lets a reference reach another thread with no ordering edge, so its fields can look unwritten. Message passing makes the send/receive pair itself the edge and the only route to the value; copy or move semantics also remove aliasing.

open as a page

Linearizability is the usual correctness bar for concurrent data structures. Define it precisely, explain why "the final state is correct" is not a sufficient standard, and contrast it with sequential consistency.

level: seniorimportance: should knowfreq 36%

basics

~20 s

An object is linearizable if every operation appears to take effect instantaneously at some instant between its call and its return, and the resulting sequence obeys the object's single-threaded specification. It constrains what concurrent observers can see, not just the end state, and unlike sequential consistency it respects real time and composes.

open as a page

What is a memory fence (barrier), what do the four barrier kinds — LoadLoad, LoadStore, StoreStore and StoreLoad — each prevent, and why is one of them far more expensive than the others?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A fence forbids specific reorderings across a point in program order. LoadLoad keeps earlier loads before later loads; StoreStore keeps earlier stores before later stores; LoadStore keeps a load before a later store; StoreLoad keeps an earlier store visible before a later load — that one must drain the store buffer, so it is the costly full fence.

open as a page

A service with an unsynchronized shared flag has run correctly for years, then starts failing after the team moves to a different CPU architecture and raises the compiler optimization level. Explain how code with a data race can appear correct for years and then break, and what you would do about it.

level: seniorimportance: should knowfreq 42%

basics

~20 s

A race has no defined behaviour; it only ever appeared to work. Strongly ordered CPUs and low optimization levels hide most reordering, and the failure window is nanoseconds wide. A weaker memory model plus a more aggressive optimizer exposes what was always permitted. Fix the synchronization; do not tune around the symptom.

open as a page

You are designing a heavily used shared data structure and a colleague proposes replacing its mutex with a hand-written non-blocking algorithm on the grounds that "locks are slow". How do you decide whether a non-blocking design is the right call, and what would you propose instead if it is not?

level: principalimportance: should knowfreq 30%

basics

~20 s

Non-blocking algorithms buy tolerance of preemption and failure and bounded tail latency, not throughput — contention still serialises in hardware. Choose them for delay-intolerant or non-blockable contexts, and prefer proven library implementations. Otherwise reduce sharing: partition per core, batch, use immutable snapshots, or keep a short-held lock.

open as a page

Some processor architectures provide load-linked and store-conditional instructions instead of a single compare-and-swap. Explain how that pair works, how it differs semantically from compare-and-swap, and what a programmer must account for when code runs on such hardware.

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Load-linked reads a location and registers a reservation on it; store-conditional writes only if nothing has touched that location since. It detects any intervening write rather than comparing values, so it is naturally immune to a value changing and changing back. It can also fail spuriously, so it must always sit in a retry loop.

open as a page

The Michael-Scott lock-free FIFO queue keeps a permanent dummy node plus separate head and tail pointers, and a thread enqueuing may find the tail pointer lagging one node behind the real last node. Why is the design shaped that way, and what does a thread do when it observes a stale tail?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

A FIFO mutates both ends, so it needs two pointers; the dummy node keeps them from aliasing when the queue is empty, so enqueue and dequeue never fight over one word. Appending takes two steps that cannot be one atomic action, so the tail can lag; any thread that sees a lagging tail advances it for the other thread before proceeding.

open as a page

You are designing a heavily read, occasionally updated shared index in a systems language with manual memory management, and must pick a memory-reclamation strategy for it: hazard pointers, an epoch-based scheme, a read-copy-update style grace period, or avoiding the problem entirely. How do you decide, and what would make you reject the lock-free design altogether?

level: principalimportance: nice to knowfreq 14%

basics

~20 s

Decide on read cost versus memory bound and on whether any reader can stall. Epoch and grace-period schemes give near-free reads but unbounded garbage if one reader blocks; hazard pointers bound memory at a per-read cost. If readers may block or memory is hard-capped, prefer hazard pointers or drop lock-free entirely.

open as a page

An engineer proposes replacing locks in a hot code path with hand-written weakly ordered atomic operations and explicit memory fences for performance. As the technical decision-maker, how do you evaluate that proposal and what conditions would you attach?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Demand evidence the synchronization is actually the bottleneck, then treat weak ordering as a contained asset: encapsulated behind a small reviewed API, justified in writing per operation, verified with race detectors and tests on both strongly and weakly ordered hardware, and owned by someone who can maintain it.

open as a page