The ABA problem is one of the classic edge cases in non-blocking algorithms.
The short version is:
- a thread reads value
A - another thread changes it to
B - then changes it back to
A - the first thread sees
Aagain and assumes nothing changed
That assumption can be wrong.
Why This Matters
Plain compare-and-set only checks whether the current value still equals the expected value.
It does not answer a stronger question:
- has this location remained untouched since I last looked?
Those are different questions. ABA appears when the difference starts to matter.
Runnable Example
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaProblemDemo {
public static void main(String[] args) {
AtomicStampedReference<String> slot =
new AtomicStampedReference<>("A", 0);
int[] observedStamp = new int[1];
String observedValue = slot.get(observedStamp);
// Another thread could do this while we are paused:
slot.compareAndSet("A", "B", 0, 1);
slot.compareAndSet("B", "A", 1, 2);
boolean updated = slot.compareAndSet(
observedValue,
"C",
observedStamp[0],
observedStamp[0] + 1);
System.out.println("Observed value = " + observedValue);
System.out.println("Observed stamp = " + observedStamp[0]);
System.out.println("CAS succeeded? " + updated);
System.out.println("Current value = " + slot.getReference());
System.out.println("Current stamp = " + slot.getStamp());
}
}
Even though the value returned to A, the stamp changed from 0 to 2.
That is why the final CAS fails. The stamped reference preserves the history signal that plain value equality loses.
Where ABA Shows Up
ABA matters most in low-level lock-free structures such as:
- stacks
- queues
- free lists
- pooled node or slot management
It is much less common in everyday business counters and flags.
That is why many application developers never see it directly until they start building custom non-blocking data structures.
Common Mitigations
Typical defenses include:
- version numbers or stamps
AtomicStampedReference- redesigning ownership so stale observations are less dangerous
- using higher-level concurrent data structures instead of custom lock-free code
In Java, garbage collection removes some memory-reclamation hazards common in lower-level languages, but it does not eliminate ABA itself.
Practical Guidance
Do not overgeneralize the problem.
ABA does not mean “CAS is unsafe.” It means:
- CAS on plain values may be too weak for certain algorithms
- some algorithms need extra state to prove freshness
For ordinary counters, flags, and immutable snapshot swaps, ABA is often irrelevant. For pointer-like coordination structures, it can be central.
Mental Model
The easiest way to understand ABA is to separate value equality from state freshness. A plain CAS operation can answer this question well:
- is the current value equal to what I expected
It cannot answer this stronger question:
- has nothing important happened since I last inspected this location
ABA is what you get when those two questions diverge.
The location ends up holding A again, so equality still matches, but the history in between may have invalidated the first thread’s plan.
That is why the problem appears mainly in algorithms where history matters, not just final value.
A Production-Shaped Example
Imagine a non-blocking stack implemented as linked nodes. Thread one reads the current head node and plans to pop it. Before it completes, another thread pops that node, does more work, and later pushes the same logical node reference back to the head. Now the head looks familiar again. A plain CAS based only on pointer equality may succeed, even though the structure changed in a way the first thread never observed.
This is the crucial reader lesson:
ABA is rarely about strings like A, B, and A in business code.
It is about stale structural assumptions in pointer-like coordination.
That is why many application engineers never hit it with counters or flags, but custom lock-free data structures absolutely can.
Testing and Review Notes
If you suspect ABA risk, review for these warning signs:
- custom non-blocking linked structures
- node reuse or pool reuse across updates
- algorithms that depend on “nothing changed since I looked”
- plain CAS on references where version history actually matters
Tests should not only assert final values.
They should exercise repeated interleavings, especially pop-push or remove-reinsert patterns that can make the final reference look unchanged.
In many teams, the most practical mitigation is not heroic testing but design restraint: use AtomicStampedReference, versioned state, or higher-level concurrent collections instead of inventing custom lock-free structures unless the need is real.
When Stamps Are Enough and When They Are Not
A version stamp is often the simplest practical fix because it upgrades the question from “is the value equal” to “is this still the same observed version of the state.” That is enough for many pointer-like coordination algorithms. But even then, the broader design still matters. If the structure is complicated enough that freshness, ownership, and reclamation are all hard to explain, the better answer may be to avoid custom lock-free design entirely.
Why Everyday Atomics Usually Escape ABA
Most application uses of atomics involve counters, flags, or immutable snapshot references where the important question is the current value, not the hidden structural history. That is why many engineers can use atomics successfully for years without hitting ABA directly. The problem becomes central only when the algorithm depends on proving that a pointer-like state did not leave and return between observations.
Reader Shortcut
If you are not building pointer-like lock-free structures, ABA is probably not your first atomic problem. That perspective matters because it keeps everyday engineering attention on the higher-value concerns: clear invariants, simple synchronization, and maintainable ownership boundaries.
Second Example: What Plain CAS Misses
The stamped example is the fix. It is also useful to see the plain reference version that looks fine but loses the history signal.
import java.util.concurrent.atomic.AtomicReference;
public class PlainAbaDemo {
public static void main(String[] args) {
AtomicReference<String> slot = new AtomicReference<>("A");
String observed = slot.get();
slot.compareAndSet("A", "B");
slot.compareAndSet("B", "A");
boolean updated = slot.compareAndSet(observed, "C");
System.out.println("Plain CAS succeeded? " + updated);
System.out.println("Current value = " + slot.get());
}
}
This is exactly the issue:
- the final value looks unchanged to the first thread
- plain equality says the update is still safe
- the missing history is what the stamp restores
Key Takeaways
- ABA means a value changed away from
Aand back toA, hiding an important intermediate change. - Plain compare-and-set may miss that history.
- Stamps or version numbers restore the missing freshness signal.
- ABA mainly matters in custom lock-free structures, not in every atomic use case.
Comments