skip to content

What is compare-and-swap (CAS), and how do classes like AtomicInteger use it to update a value safely without locks?

level: middleimportance: must knowfreq 78%

answer

  1. Compare expected, swap only if it matches, return success/fail
  2. Read → compute → compareAndSet → retry loop = optimistic concurrency
  3. Lock-free / non-blocking, no deadlock
  4. One cell only; ABA needs a stamp
  5. High contention spins → use LongAdder

basics

~20 s

CAS is an atomic operation that updates a value only if it still equals an expected old value. AtomicInteger uses it in a small retry loop, so threads can increment safely without taking a lock.

solid answer

~40 s

Compare-and-swap (CAS) is a single hardware-level atomic operation: "set this memory location to a new value, but only if it currently holds the value I expected." It returns whether the swap happened. Atomic classes (AtomicInteger, AtomicLong, AtomicReference) build lock-free updates on top of it. For incrementAndGet, the class reads the current value, computes value+1, then calls compareAndSet(expected, new); if another thread changed the value in between, the swap fails and the operation retries with the fresh value in a loop. Because the whole compare-and-set is atomic, no lock is needed: there's no critical section that another thread can interleave with. This gives non-blocking progress, avoids deadlock, and is usually faster than synchronized under moderate contention. The cost is a busy retry loop that can spin under very high contention.

code

java · 14 lines
java
AtomicInteger counter = new AtomicInteger(0);

// Built-in atomic increment (CAS retry loop is hidden inside):
int v = counter.incrementAndGet();

// Equivalent hand-written optimistic loop:
int current, next;
do {
    current = counter.get();
    next = current + 1;
} while (!counter.compareAndSet(current, next)); // retry if another thread won the race

// updateAndGet runs the loop for you with a function:
counter.updateAndGet(x -> x * 2 + 1);

go deeper

for a junior

Knows AtomicInteger exists and gives a thread-safe counter, and that plain count++ is not safe. Can call incrementAndGet without explaining CAS internals.

for a middle

Can define CAS (compare expected, swap if equal, report success), describe the read-compute-compareAndSet retry loop, and explain why it is lock-free and how it avoids the lost-update race.

for a senior

Explains optimistic concurrency vs locking trade-offs, the visibility (volatile-like) guarantee, single-cell-only limitation, when CAS spins under contention (and reaching for LongAdder), and updateAndGet/accumulateAndGet.

for a principal

Connects CAS to the hardware (CMPXCHG / LL-SC), the Java Memory Model happens-before edges, ABA and stamped references, and reasons about when lock-free designs beat lock-based ones (latency tails, deadlock-freedom) vs their complexity and starvation/livelock risks.

## The problem CAS solves Suppose two threads both run `count++` on a shared `int`. `count++` is **not atomic** — it is really three steps: (1) read `count` into a register, (2) add 1, (3) write the result back. If thread A reads 5, then thread B reads 5, both compute 6, and both write 6 — one increment is **lost**. This is a *race condition* on a *read-modify-write* (RMW) operation. The classic fix is a **lock** (`synchronized`): only one thread at a time may run the three steps. That works but is *blocking* — a thread that holds the lock and gets descheduled forces every other thread to wait. ## What compare-and-swap is **Compare-and-swap (CAS)** — also called *compare-and-set* in Java — is a single operation the CPU performs **atomically** (indivisibly, as one uninterruptible unit). Conceptually it does: ``` CAS(memoryCell, expectedValue, newValue): if memoryCell == expectedValue: memoryCell = newValue return true // swap succeeded else: return false // someone else changed it; nothing written ``` The key point: the *compare* and the *swap* happen together as one indivisible step that no other thread can interleave with. Modern CPUs expose this as a machine instruction (e.g. `CMPXCHG` on x86, `LL/SC` load-linked/store-conditional on ARM). In Java it surfaces as `boolean compareAndSet(expected, newValue)` on the atomic classes. ## How AtomicInteger uses it (the retry loop) `AtomicInteger.incrementAndGet()` works like this: ```java int current; do { current = get(); // read the latest value int next = current + 1; // compute new value } while (!compareAndSet(current, next)); // try to swap; retry if it failed return next; ``` Walk through the race again. Thread A reads 5, thread B reads 5. Both want to write 6. Suppose B's CAS runs first: the cell is still 5, so `CAS(5, 6)` succeeds, cell becomes 6. Now A's `CAS(5, 6)` runs: the cell is **no longer 5** (it's 6), so A's CAS *returns false*. A loops, re-reads 6, computes 7, and `CAS(6, 7)` succeeds. No increment is lost, and **no lock was ever taken**. This is called **optimistic concurrency**: assume no conflict, do the work, and only redo it if the swap detects a conflict. ## Lock-free and non-blocking Because there is no lock, this style is **lock-free / non-blocking**: a thread that gets descheduled mid-loop cannot block others — the others' CAS attempts simply succeed. The system as a whole always makes progress (some thread always wins each round). Benefits: no deadlock, no lock-acquisition overhead, often better throughput than `synchronized` under low-to-moderate contention. ## The atomic class family - `AtomicInteger`, `AtomicLong` — atomic numeric counters with `incrementAndGet`, `getAndAdd`, `addAndGet`, `getAndSet`, `updateAndGet(IntUnaryOperator)`, `accumulateAndGet`. - `AtomicBoolean` — atomic flag. - `AtomicReference<V>` — atomic update of an object reference; the building block for lock-free data structures. - (`AtomicIntegerArray`/`AtomicReferenceArray` for element-wise atomicity.) `updateAndGet`/`accumulateAndGet` take a function and run the read-compute-CAS loop **for you**, so you express "apply this function atomically" without hand-writing the loop. ## Costs and caveats - **Spin under high contention.** If hundreds of threads hammer one counter, many CAS attempts fail and retry, wasting CPU. For very hot counters prefer `LongAdder`, which spreads updates across multiple cells. - **Only single-variable atomicity.** CAS atomically updates **one** cell. To update two related fields atomically you must put them behind a single `AtomicReference` (e.g. to an immutable holder object) or use a lock. - **The ABA problem.** CAS only checks that the value *equals* the expected value, not that it never changed. If it went A→B→A, a stale CAS(A, …) still succeeds even though the world moved. Mitigated with `AtomicStampedReference` (adds a version stamp). Numeric counters that only ever increase don't hit ABA in practice. - **Visibility.** Atomic-class reads/writes have the same memory-visibility guarantees as `volatile`, so updates are correctly published to other threads.

  • Why is `volatile int count; count++;` still not thread-safe?
    `volatile` guarantees visibility and atomic reads/writes of the single field, but `count++` is a read-modify-write composed of three steps. Two threads can both read the same value and both write back the same incremented value, losing an update. You need an atomic CAS-based operation (AtomicInteger.incrementAndGet) or a lock for the compound operation.
  • When would you prefer LongAdder over AtomicInteger/AtomicLong?
    Under high write contention (many threads incrementing the same counter), AtomicLong's single CAS cell becomes a hotspot and retries spin. LongAdder spreads updates across multiple internal cells, so threads rarely collide, trading exact intermediate reads (sum() aggregates cells) for much higher throughput. Prefer it for hot counters where you mostly write and read the total occasionally.

saying these in an interview costs you the question

  • Saying CAS "locks" the variable — it is explicitly lock-free, the whole point is no lock
  • Claiming count++ is atomic on a plain int or even a volatile int (volatile fixes visibility, not the RMW race)
  • Thinking CAS can atomically update two variables at once — it updates a single cell only
  • Believing a successful CAS proves the value never changed (ignoring the ABA problem)
  • Confusing AtomicInteger with synchronization that blocks; failed CAS retries, it does not block

context