Walk through how the ABA problem corrupts a lock-free Treiber stack, and how stamping prevents it.
answer
- Treiber stack = single CAS'd head, push/pop retry loops
- Read head=A, preempt, other thread pops A,B and re-pushes A
- CAS(head, A, B) wrongly succeeds -> resurrects popped node, leaks C
- Stamp advances 10->13 so CAS(.,.,10,11) fails -> retry
- GC stops address reuse, not legitimate re-push (logical ABA)
basics
~20 sIn a lock-free stack, a thread reads head=A and plans to set head to A.next. If another thread pops A and B then pushes A back, head is A again with a different next. The first thread's CAS still succeeds, setting head to a stale node. A version stamp makes that CAS fail instead.
solid answer
~50 sA Treiber stack is a lock-free stack whose top is a single 'head' reference; push/pop are CAS retry loops. Pop reads head=A, computes newHead=A.next, then CAS-es head from A to A.next. The ABA corruption: after Thread 1 reads head=A but before its CAS, Thread 2 pops A, pops A.next (B), then pushes A back. Now head==A again, but the stack is {A→C} not {A→B}; A.next that Thread 1 captured is the stale B (which may even be off the stack). Thread 1's compareAndSet(A, B) sees head==A and succeeds, setting head=B and resurrecting a removed node — corrupting the stack. Wrapping head in an AtomicStampedReference fixes it: every push/pop bumps the stamp, so by the time Thread 1 CAS-es, the stamp has advanced past the one it read; the (reference, stamp) compare fails on the stamp even though the reference matches, forcing a re-read and recompute. Java's GC reduces but doesn't remove this, since A can be legitimately re-pushed.
go deeper
Can repeat that a popped-and-re-pushed head makes a stale CAS succeed; not expected to trace node leakage in detail.
Traces the read-head, preempt, pop/pop/push interleaving and states that a stamp makes the CAS fail; identifies that the stale next pointer is the problem.
Explains the full corruption (resurrected node + lost C), the GC mitigation boundary, and writes the AtomicStampedReference-based pop with the stamp advancing to defeat it.
Frames ABA as value-equality-vs-identity-over-time, weighs stamping's allocation cost against hazard pointers, epoch reclamation, and double-width CAS, and reasons about when designing for provable benignness beats versioning.
## The structure: a Treiber stack A **Treiber stack** is the classic lock-free stack. It keeps a single mutable field, `head`, pointing at the top node; each node has a `next` pointing at the node below. Both operations are **read-compute-CAS retry loops**: ```text push(x): loop { oldHead = head; node.next = oldHead; if CAS(head, oldHead, node) break } pop(): loop { oldHead = head; if oldHead == null return null; newHead = oldHead.next; if CAS(head, oldHead, newHead) return oldHead } ``` No locks; correctness rests entirely on CAS reliably failing when `head` changed. ## The ABA interleaving, step by step Start with the stack `head -> A -> B -> C` (A on top). 1. **Thread 1** begins `pop()`: reads `oldHead = A`, computes `newHead = A.next = B`. It is about to `CAS(head, A, B)` — then it is **preempted** (descheduled). 2. **Thread 2** runs to completion: - `pop()` removes **A**. Stack is now `head -> B -> C`. - `pop()` removes **B**. Stack is now `head -> C`. - `push(A)` puts **A** back on top (A's `next` is now set to C). Stack is now `head -> A -> C`. 3. **Thread 1** resumes and executes `CAS(head, A, B)`. The current `head` **is** A (it came back!), so the **CAS succeeds**. It sets `head = B`. ## Why that is catastrophic After step 3 the stack is `head -> B -> ???`. But **B was already popped** in step 2 — it is no longer a live stack node, and `B.next` is whatever it was when B left (it pointed at C, but C's own state may have moved on, or B may have been handed to a caller and mutated). The stack now contains a **resurrected, removed node**, and node **C is lost** (leaked / unreachable from head). Subsequent operations traverse corrupted links: you can lose elements, return already-popped nodes, or build a cycle. All of this happened even though every individual CAS 'succeeded' — the algorithm's safety invariant (a successful CAS means 'head didn't change since I read it') was **violated** because head changed *and changed back*. ## Note on memory reuse vs Java GC The nastiest classic form needs **node reuse**: a freed node A is recycled (an object pool, or `malloc/free` in C giving back the same address). Java's **garbage collector** mitigates *that* specific form — while Thread 1 holds a reference to A, A cannot be collected and reissued as a *different* object, so 'A is now a different node at the same address' can't happen in pure GC'd Java. **But the bug above needs no reuse** — A is the *same* object legitimately re-pushed. So GC reduces one variant; **logical ABA remains** and the corruption is real. ## The fix: stamp the head Replace `AtomicReference<Node> head` with `AtomicStampedReference<Node> head`. Now every successful push/pop increments a version **stamp** alongside the reference: ```java AtomicStampedReference<Node> head = new AtomicStampedReference<>(null, 0); Node pop() { int[] holder = new int[1]; while (true) { Node old = head.get(holder); int stamp = holder[0]; if (old == null) return null; Node nxt = old.next; if (head.compareAndSet(old, nxt, stamp, stamp + 1)) return old; } } ``` Replay the interleaving: Thread 1 reads `(A, stamp=10)`. Thread 2's two pops and one push bump the stamp to **13**. Thread 1 resumes and does `compareAndSet(A, B, 10, 11)`. The reference matches (head==A) but the **stamp is now 13, not 10**, so the **CAS fails**. Thread 1 loops, re-reads `(A, 13)`, recomputes `A.next` (now C), and retries correctly. The intervening change is detected precisely because the stamp **monotonically advanced** even though the reference returned to its old value. ## Cost and alternatives at scale Stamping boxes the (reference, stamp) pair into an immutable holder swapped atomically, so each successful update allocates a small object — measurable under very high contention. Production-grade lock-free reclamation often prefers schemes that avoid both ABA and that allocation: **hazard pointers** and **epoch-based reclamation** make it safe to know when a node can be reused (so the address-reuse form of ABA can't arise), and **double-width / 128-bit CAS** (where the hardware supports it) packs a counter beside the pointer for free. The principal-level takeaway: ABA is fundamentally 'CAS tests value equality, but the invariant you need is identity-over-time,' and you close the gap either by versioning (stamps), by controlling reuse (hazard pointers/epochs), or by proving the value's return is semantically harmless.
- If Java's GC prevents node A from being reused as a different object, why does the stack still corrupt?Because this interleaving doesn't rely on reuse-as-a-different-object: A is the same object, legitimately popped and re-pushed, so its reference truly returns to head. GC prevents the 'same address, different node' variant common in C/C++, but it can't prevent a value from legitimately cycling back to a prior reference, which is enough for the captured stale A.next to corrupt the stack.
- Besides stamping, what production techniques avoid this in high-performance lock-free code?Hazard pointers and epoch-based reclamation track when a node is safe to free/reuse, eliminating the address-reuse form of ABA and the need to stamp; double-width (128-bit) CAS packs a counter beside the pointer where hardware supports it; and sometimes you design the structure so a returned value is provably harmless. Each trades complexity, memory, or portability differently than AtomicStampedReference's per-update allocation.
saying these in an interview costs you the question
- Concluding GC makes the Treiber stack ABA-safe — it doesn't, because legitimate re-push needs no reuse.
- Saying the CAS 'should have failed because head changed' — the whole point is it changed BACK so a value-only CAS succeeds.
- Forgetting that node C is leaked/lost, not just that a stale node reappears.
- Claiming a single mark (AtomicMarkableReference) would fix the stack — it needs a monotonic stamp, not one bit.