What is the ABA problem in CAS-based code, and how do you guard against it in Java?
answer
- A→B→A: value matches but history changed; stale CAS wrongly succeeds
- CAS checks value equality, not change history
- Harmless for monotonic counters; dangerous for node-reuse lock-free structures
- Fix: AtomicStampedReference — CAS the (ref, version stamp) pair, bump stamp each update
- AtomicMarkableReference = single boolean mark variant
basics
~20 sABA is when a value changes from A to B and back to A, so a CAS expecting A succeeds even though the data really changed underneath. In Java you guard against it with AtomicStampedReference, which adds a version number you also compare.
solid answer
~50 sCAS only checks that the current value *equals* the expected value, not that it never changed. The ABA problem is when another thread changes the value A→B→A between your read and your CAS: your `compareAndSet(A, …)` still succeeds, even though the underlying state was mutated in between. For a plain monotonically-increasing counter this is harmless, but for lock-free structures (e.g. popping a node off a stack and reusing it) ABA can corrupt the structure — you swap in a stale node believing nothing changed. Java's fix is `AtomicStampedReference<V>`, which pairs the reference with an `int` **stamp** (version); every successful update bumps the stamp, and `compareAndSet(expectedRef, newRef, expectedStamp, newStamp)` requires *both* to match. Because the stamp keeps increasing, an A→B→A cycle leaves a different stamp, so the stale CAS fails. `AtomicMarkableReference` is the lighter variant carrying a single boolean mark instead of a counter.
code
java · 11 linesAtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String value = ref.get(stampHolder); // reads value AND its stamp
int stamp = stampHolder[0];
// Meanwhile another thread did A -> B -> A, bumping the stamp twice.
// Our CAS requires BOTH the reference and the original stamp to match:
boolean ok = ref.compareAndSet(value, "C", stamp, stamp + 1);
// -> false, because the stamp advanced; we detect the hidden change and retry.go deeper
Generally not expected to know ABA. Recognizing the term and that 'CAS only checks the current value' is a bonus.
Can state the A→B→A definition and that CAS checks value equality not history, and name AtomicStampedReference as the Java fix at a high level.
Explains where ABA actually bites (lock-free node-reuse structures vs harmless monotonic counters), and uses AtomicStampedReference (and AtomicMarkableReference) correctly, bumping the stamp each update.
Reasons about identity-vs-history, designs or reviews lock-free structures for ABA hazards, weighs stamped references vs avoiding reuse vs GC-based safety, and is aware of stamp wrap-around and the limits of a single boolean mark.
## Recap: what CAS actually verifies `compareAndSet(expected, newValue)` performs an atomic *compare-then-conditional-write*: it writes `newValue` **only if** the cell currently equals `expected`. Crucially, it checks **equality of the value right now**, not the *history* of the cell. It cannot tell the difference between "the value never changed" and "the value changed several times but happens to equal `expected` again now." ## The ABA scenario Name the value transitions by letters: 1. Thread T1 reads the shared cell and sees **A**. It intends to do `CAS(A, …)` shortly. 2. Before T1's CAS, thread T2 changes the cell **A → B**, then changes it **B → A** again. 3. T1 now runs `CAS(A, …)`. The cell currently holds **A**, so the CAS **succeeds** — even though the world moved through B and back. T1 believes "nothing changed since my read," which is **false**. That is the **ABA problem**: same value, different history. ## Why it's often harmless — and when it isn't For a counter that only ever **increases** (or otherwise never revisits a prior value), ABA can't occur: the value can't come back to a number it already passed. So `AtomicInteger`/`AtomicLong` counters are fine. ABA bites **lock-free data structures that recycle objects**. Classic example: a lock-free Treiber stack. ```text Stack: head -> A -> C T1: pop() reads head = A, plans CAS(head, A, A.next == C) T1 is descheduled here. T2: pop A (head -> C) T2: pop C (head -> empty) T2: push A (head -> A -> empty) // A reused, but A.next is now stale T1 resumes: CAS(head, A, C) succeeds because head == A again, but C was already removed -> head now points at a freed/stale node. Corruption. ``` The reference `A` *equals* the expected `A`, but its meaning (its `next`) changed. CAS can't see that. ## The fix: a version stamp The cure is to make every modification observable even when the value returns to a prior one — attach a **monotonically increasing version counter (stamp)** to the value and CAS the **(value, stamp)** pair together. Any A→B→A round trip bumps the stamp twice, so the stamp differs from what T1 read, and T1's combined CAS fails. Java provides this as **`AtomicStampedReference<V>`**: ```java AtomicStampedReference<Node> head = new AtomicStampedReference<>(a, 0); int[] stampHolder = new int[1]; Node current = head.get(stampHolder); // read value AND stamp int stamp = stampHolder[0]; // ... compute newValue ... boolean ok = head.compareAndSet( current, newValue, // expected vs new reference stamp, stamp + 1); // expected vs new stamp (bump it) ``` `compareAndSet` succeeds only if **both** the reference **and** the stamp still match. Because each successful update increments the stamp, the A→B→A cycle leaves a stamp that no longer equals T1's expected stamp, so the stale CAS fails and T1 retries with fresh state. ## `AtomicMarkableReference` When you don't need a full counter — only a single boolean flag travelling atomically with the reference (e.g. "this node is logically deleted") — use **`AtomicMarkableReference<V>`**, which carries one `boolean` *mark* instead of an `int` stamp. It's used in lock-free list deletion algorithms (mark-then-unlink). Note: a single boolean can still ABA on the mark itself; the stamp (counter) is the stronger guard. ## Practical guidance - Most application code never meets ABA because it isn't writing lock-free recycling structures — prefer the JDK's concurrent collections, which already handle it. - If you *are* building lock-free structures with node reuse, assume ABA is possible and use a stamped reference (or avoid reuse, e.g. allocate fresh immutable nodes / let GC prevent reuse of live addresses). - ABA is fundamentally about *identity vs history*: CAS guards identity-now, the stamp restores a notion of history.
- Why doesn't a simple incrementing AtomicLong counter suffer from ABA?ABA requires the value to return to a previously-seen value. A counter that only increases never revisits an earlier number, so the A→B→A pattern can't occur for it. ABA is a hazard for values that can cycle back — typically reused object references in lock-free structures, not monotonic counters.
- How does AtomicStampedReference make the stamp itself immune to wrap-around or reuse?It doesn't fully — the int stamp can in theory wrap after ~4 billion updates and collide. In practice the window for an ABA with the exact same stamp is astronomically unlikely, and you bump the stamp on every update so collisions require both the reference and the stamp to coincide at the same instant. For stronger guarantees you'd use a wider counter or a design that avoids address reuse.
saying these in an interview costs you the question
- Believing a successful CAS proves the value was untouched since your read
- Thinking ABA affects ordinary AtomicInteger counters in normal use (monotonic values can't ABA)
- Confusing the fix: ignoring the stamp and only comparing the reference still allows ABA
- Assuming AtomicMarkableReference's single boolean fully prevents ABA (the mark itself can ABA; the stamp counter is stronger)
- Hand-rolling lock-free recycling structures without considering ABA at all