Concurrency
Atomic operations and CAS: interview questions and how to answer them
Compare-and-swap is a single hardware instruction that writes a new value only if the current value is still the one you read — the primitive every lock-free algorithm and every lock is built on.
Written and reviewed by Sahil Srivastav
What it actually is
An atomic operation is one that no other thread can observe half-completed. The hardware provides a small set: atomic load and store of a word, atomic exchange, fetch-and-add, and compare-and-swap. CAS takes an address, an expected value, and a new value; it writes the new value and reports success only if the memory still held the expected one. On x86 this is `LOCK CMPXCHG`; on ARM it is a load-linked/store-conditional pair.
Everything above it is composition. `AtomicInteger.incrementAndGet` is a loop: read the current value, compute value + 1, CAS; if the CAS fails because someone else moved it, read again and retry. The loop is what makes the operation *atomic in effect* even though it is not a single instruction — no thread ever observes an intermediate state, because a losing thread discards its work rather than publishing it.
This is where lock-free gets its guarantee. A lock-free algorithm guarantees that *some* thread makes progress at every step, because a failed CAS means someone else succeeded. It does not guarantee that a *given* thread makes progress — an unlucky thread can retry indefinitely, which is why lock-free is weaker than wait-free and why a CAS loop is not automatically fast.
Why it matters in production
Because it is the mechanism that removes blocking from hot paths without removing correctness. A counter behind a lock forces every incrementing thread through a context switch under contention; a CAS-based counter lets them all run and lets one win per round. At low to moderate contention that is dramatically faster, and it never parks a thread, so a thread killed or descheduled mid-operation cannot block anyone else — a property a lock cannot offer.
Because it is also the mechanism that inverts under high contention, and knowing where the crossover lies is what distinguishes experience from enthusiasm. Every CAS on a contended line invalidates that line in every other core's cache, so N threads hammering one `AtomicLong` spend their time bouncing a cache line between cores. Beyond a certain contention level a lock is faster, because parking threads reduces the traffic. `LongAdder` exists precisely because `AtomicLong` loses at high write contention.
And because CAS is the same idea as an optimistic database update, a conditional HTTP `If-Match`, and a Redis `WATCH`/`MULTI` transaction. A candidate who sees the pattern across those layers is the one who will design a conditional write correctly in a system that has no locks at all.
How it works
The CAS retry loop, written out
Read the current value into a local. Compute the new value from it. CAS from old to new. If the CAS fails, the value moved, so discard the computation and start again from a fresh read. The correctness comes from never writing a value derived from a stale read — a failed CAS is not an error, it is the mechanism working.
ABA: the value matches but the world moved
CAS compares values, not histories. If a thread reads A, another changes it to B and back to A, the CAS succeeds even though the intervening state change mattered. For a counter this is harmless. For a pointer-based structure it is a correctness bug — the node you are about to link may have been freed and reallocated. The fix is to CAS a value plus a version stamp together: `AtomicStampedReference` in Java, a tagged pointer in C.
Contention is a cache-coherence problem
A successful CAS requires exclusive ownership of the cache line, so every attempt from another core forces an invalidation round trip. Throughput on a single contended atomic therefore falls as cores increase. Striping the state across lines — one cell per thread, summed on read — converts contention into independent updates, which is exactly what `LongAdder` does.
Atomic fields also carry volatile semantics
Every `Atomic*` get is a volatile read and every set is a volatile write, so they give visibility and ordering in addition to atomicity. That is why an `AtomicReference` is a correct safe-publication mechanism, and why you do not need to add `volatile` alongside one.
Weaker orderings exist and are easy to misuse
`lazySet`/`setRelease` skips the full barrier of a volatile write and is appropriate when you only need release semantics — nulling out a reference you will never read again, for example. `weakCompareAndSet` may fail spuriously. Both are correct only inside a retry loop or an already-established ordering, and reaching for them without measuring is how subtle bugs arrive.
Implementing it
Use `AtomicInteger`/`AtomicLong` for counters and flags, and `LongAdder` when a counter is written by many threads and read rarely — metrics, request counts. The adder trades a cheap read for much cheaper writes.
Use `AtomicReference.updateAndGet` with an immutable value object to make multi-field state transitions atomic without a lock. Building a new immutable snapshot and CASing the reference is a clean pattern that avoids partially updated state entirely.
Bound CAS loops in code where the computation is expensive, and consider falling back to a lock after a few failures. A loop whose body costs microseconds is a livelock waiting to happen under contention.
Pad or separate hot atomics that share a cache line. Two unrelated counters in adjacent fields will contend through false sharing even though no thread touches both; `@Contended` or deliberate padding removes it.
// What incrementAndGet is doing underneath
int current, next;
do {
current = value.get();
next = current + 1;
} while (!value.compareAndSet(current, next)); // retry on a lost race
// Multi-field atomic transition without a lock: CAS an immutable snapshot
record State(long total, int count) {}
final AtomicReference<State> state = new AtomicReference<>(new State(0, 0));
void record(long v) {
state.updateAndGet(s -> new State(s.total() + v, s.count() + 1));
}
// ABA-sensitive: compare a stamp alongside the reference
AtomicStampedReference<Node> head = new AtomicStampedReference<>(null, 0);
int[] stamp = new int[1];
Node h = head.get(stamp);
head.compareAndSet(h, h.next, stamp[0], stamp[0] + 1);
// Many writers, few readers: shard the cells instead of contending on one
final LongAdder requests = new LongAdder();
requests.increment();Interview questions and how to answer them
Explain how `AtomicInteger.incrementAndGet` works.
It is a CAS retry loop, not a single instruction. Read the current value, compute value plus one, then compare-and-swap from the value you read to the value you computed. If another thread changed it in between, the CAS fails, you throw away the computed value, re-read, and try again. The reason this is correct is that you never publish a result derived from a stale read — a losing thread discards its work. Modern JDKs may lower it to `LOCK XADD` on x86 for the plain increment case, which is a single instruction, but the general `updateAndGet` shape is the loop.
What is the ABA problem and when does it actually bite?
CAS compares the current value with the expected value, so it cannot tell "unchanged" from "changed and changed back". If I read pointer A, another thread pops A, pushes B, and pushes A again, my CAS on A succeeds against a structure whose shape is different from what I reasoned about — in a lock-free stack that corrupts the list. It does not bite counters, because for a counter the value is the whole state. The fix is to make the comparison include history: an `AtomicStampedReference` that CASes reference and version together, so a return to A with a bumped stamp fails.
Is a CAS loop always faster than a lock?
No, and the crossover is contention. With few threads, CAS wins clearly — no context switches, no parking, no scheduler involvement. As contention rises, every attempt requires exclusive ownership of the cache line, so successful work per unit time falls while cache traffic climbs, and a lock that parks losers can beat it because parked threads generate no traffic. If the critical section is long, a lock wins sooner still, since a long CAS body means a long window to be beaten. The practical answer is to measure at the concurrency you actually run at.
You have a request counter incremented by 200 threads and read once a second. What do you use?
`LongAdder`. `AtomicLong` would funnel all 200 writers through one cache line, so they serialise on coherence traffic rather than on a lock. `LongAdder` keeps an array of cells, routes each thread to a cell by a probe hash, and sums them on `sum()` — so writes are almost uncontended and the read pays the aggregation. It is the right trade exactly when writes vastly outnumber reads and the read does not need a precise instantaneous snapshot.
How is CAS related to optimistic locking in a database?
They are the same idea at different layers. `UPDATE t SET v = $new, version = version + 1 WHERE id = $1 AND version = $old` is a compare-and-swap where the comparison is a predicate and the affected-row count is the success flag. The same properties follow: no lock is held across think time, a failure means someone else won, the retry must recompute from a fresh read, and it degrades badly under high contention on one row. Recognising it as CAS also tells you the failure modes to ask about — ABA arrives as a row that changed and changed back, which is why a monotonic version beats comparing the data.
Answers that lose the round
- Describing CAS as "a lock without blocking" — it is a conditional write, and the retry loop is the part that does the work
- Treating a failed CAS as an error to log or throw on, rather than as the normal signal to re-read and retry
- Computing the new value once and retrying the CAS with the same stale value, which either spins forever or writes a wrong result
- Claiming lock-free means faster; it means no thread can block another, and under heavy contention a lock often wins
- Assuming CAS solves ABA, or not knowing what ABA is when asked about a lock-free stack
- Using `AtomicLong` for a hot metric counter across dozens of threads instead of `LongAdder`, then blaming the JVM for the cache-line traffic
- Wrapping two atomic operations in sequence and calling the pair atomic
Practise atomic operations and cas in a real repository
Gronex ships this as a runnable repository: an inventory path whose read-modify-write loses updates under concurrency. The tests assert stock never goes negative across many concurrent buyers, so only a genuinely atomic conditional update passes.
FAQ
Are locks implemented with CAS?
Yes, at the bottom. A mutex's fast path is a CAS on a state word from unlocked to locked; only when that fails does it fall back to the OS to park the thread. Java's `ReentrantLock` is built on `AbstractQueuedSynchronizer`, which is a CAS-managed state word plus a CAS-managed wait queue. So "lock versus CAS" is really "park the loser versus retry the loser".
What is the difference between lock-free and wait-free?
Lock-free guarantees system-wide progress: at every step some thread completes, because a failed CAS implies another succeeded. Wait-free guarantees per-thread progress in a bounded number of steps, so no thread can be starved. Wait-free algorithms are far harder and usually slower in practice; almost everything described as lock-free in production is lock-free, not wait-free.
Does CAS need `volatile` on the field?
In Java you do not manage this yourself when using the `Atomic*` classes — their internal field is volatile and every get/set carries the corresponding semantics. If you use `VarHandle` or the older `AtomicIntegerFieldUpdater` against your own field, that field must be `volatile`, and the API enforces it.