Atomic Variables
Atomic Variables
In the previous lessons you saw that a bare int counter shared between threads produces incorrect results because the read-modify-write sequence is not atomic. You also saw that synchronized fixes this, but it comes with a cost: it serialises all threads through a lock, which can become a bottleneck under high contention.
Java provides a middle road through the java.util.concurrent.atomic package: lock-free, thread-safe variables that rely on hardware-level atomic instructions — specifically Compare-And-Set (CAS) — instead of a mutex. This lesson covers the most important members of that package and explains when and why to reach for them.
The Compare-And-Set (CAS) Primitive
CAS is a single CPU instruction that does three things atomically:
- Read the current value of a memory location.
- Compare it to an expected value.
- If they match, write a new value; if they do not match, do nothing.
The operation returns whether the swap succeeded. The caller loops — spinning — until it wins the race. Because the check and the swap happen as a single hardware instruction, no other thread can interleave between them.
AtomicInteger — the Workhorse
AtomicInteger wraps an int and exposes every common mutation as an atomic operation. The most useful methods are:
get()/set(int)— read or write with full memory visibility.incrementAndGet()— atomically adds 1, returns the new value.getAndIncrement()— atomically adds 1, returns the old value (likei++).addAndGet(int delta)— atomically addsdelta, returns the new value.compareAndSet(int expected, int update)— performs the raw CAS; returnstrueon success.updateAndGet(IntUnaryOperator)— applies a lambda atomically (Java 8+).
Here is the same counter you saw break earlier, now written with AtomicInteger:
No synchronized, no Lock, yet the result is always exactly 200 000.
Manual CAS Loop with compareAndSet
compareAndSet is the building block for more complex lock-free logic. Suppose you want to atomically cap a counter at a maximum:
compareAndSet. If CAS fails, another thread changed the value between your read and your write, so start over. For in-memory arithmetic, the retry loop almost never runs more than once under realistic contention.
The Rest of the Atomic Family
The package contains analogues for other primitive types and for references:
AtomicLong— same API asAtomicIntegerbut forlong.AtomicBoolean— useful for a flag that many threads read but only one should flip.AtomicReference<V>— CAS on an object reference; perfect for swapping an immutable snapshot atomically.AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray<E>— per-element atomic access inside an array.
An AtomicBoolean is commonly used as a once-only flag:
LongAdder — for High-Throughput Counters
Under extreme contention many threads spinning on the same AtomicLong still cause cache-line bouncing — every successful CAS invalidates the cache line in every other CPU core, forcing retries. Java 8 introduced LongAdder (and DoubleAdder) to solve this: it maintains a cell per thread internally and adds all cells together only when you call sum(). Writes never contend with each other; reads pay a small aggregation cost.
LongAdder is not a drop-in replacement for AtomicLong. It does not support compareAndSet, and sum() is not a consistent snapshot if increments are still happening concurrently. Use LongAdder when you only need a running total; use AtomicLong when you need to read and conditionally update in one step.
AtomicReference and the ABA Problem
CAS on references has a subtle pitfall: if a reference changes from A to B and then back to A, a CAS that expects A will succeed even though the object was replaced in the meantime. This is the ABA problem. The fix is AtomicStampedReference<V>, which pairs the reference with an integer stamp (version counter) — both the reference and the stamp must match for the CAS to succeed.
When to Choose Atomics over synchronized
- Use atomics when the protected operation is a single arithmetic or reference update. They are simpler and faster under low-to-moderate contention.
- Use
synchronizedorReentrantLockwhen you need to guard a multi-step sequence as a single unit (e.g., check a condition and update two fields together). - Use
LongAdderwhen you have a pure increment counter under very heavy parallel load and you do not need CAS semantics.
Summary
Atomic variables replace locks for single-variable updates by using the CPU-level CAS instruction. AtomicInteger and AtomicLong cover arithmetic; AtomicBoolean handles flags; AtomicReference swaps object pointers safely. The spin-retry loop — read, compute, compareAndSet, repeat on failure — is the universal pattern for building lock-free logic. For pure high-volume counters, LongAdder goes further by eliminating contention through per-thread cells. Choose the right tool for the operation size: atomics for single-variable logic, locks for multi-step critical sections.