Java Atomics: AtomicInteger, AtomicReference, LongAdder
java.util.concurrent.atomic gives lock-free single-variable updates: AtomicInteger / AtomicLong / AtomicReference for counters and references, LongAdder for high-contention counters, AtomicReferenceArray for per-element ordering. All built on hardware compare-and-swap.
What it is
java.util.concurrent.atomic is a small package with one job: provide lock-free, atomic, visibility-correct operations on single variables. It has been the foundation of high-throughput Java code since Java 5.
The package ships wrappers for the obvious primitives (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference), arrays of those (AtomicIntegerArray, etc.), high-contention counters (LongAdder, DoubleAdder), and tagged references for ABA protection (AtomicStampedReference, AtomicMarkableReference).
Under the hood, every operation either reads/writes through a memory barrier (volatile semantics) or runs a hardware compare-and-swap. There is no lock, no thread parking. A failed CAS just retries.
When to reach for which
AtomicInteger / AtomicLong for a counter that is read about as often as it is written. Hit counters that show up in headers. Rate-limit buckets. Sequence numbers. Each operation is O(1), each read is one volatile load.
LongAdder for a counter that is written by many threads and read rarely. Metrics counters scraped every few seconds. Application-wide throughput measurements. Internally sharded into per-thread cells, so writes do not contend. sum() walks all cells, so reads are O(num cells).
AtomicReference for any reference swapped atomically. Configuration objects that hot-reload. Routing tables. Strategy objects in pluggable systems. Pair with immutable values for thread-safe publication.
AtomicStampedReference for lock-free data structures that must protect against ABA. Used internally by some lock-free queue implementations. Application code rarely touches it.
The CAS retry pattern
Most non-trivial atomic operations end up as a CAS retry loop. The shape is always the same:
loop {
current = atomic.get()
new = compute(current)
if (atomic.compareAndSet(current, new)) return
}
If the CAS fails, another thread won. Re-read the latest value, compute the new value again, retry. Bounded by other threads' progress, so it terminates.
Java 8 added updateAndGet, accumulateAndGet, and friends that wrap this loop. Use them when the update logic is a pure function. Hand-roll the loop when an early-exit is needed (the atomic-max example above).
Where this fits with locks
Atomic operations are for single-variable invariants. When code has to keep two fields consistent (a balance and an audit log, a head and a tail pointer), a single atomic is not enough. Either move to a lock, or change the design so all the dependent state lives in one immutable object that gets swapped atomically.
The strength of atomics is what they take off the table: no thread parking, no priority inversion, no deadlock between two atomics. The cost is that they only protect one field at a time.
A note on contention
Under low contention, AtomicLong is faster than any lock-based counter. Under high contention (many threads incrementing the same counter on a multi-core machine), CAS starts to fail repeatedly, the cache line ping-pongs between cores, and throughput drops. That is exactly the scenario LongAdder solves. The lesson generalises: when an atomic becomes a hotspot, the next step is sharding, not abandoning lock-free.
Primitives by language
- AtomicInteger / AtomicLong / AtomicBoolean / AtomicReference
- AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray
- LongAdder / DoubleAdder (high-contention counters)
- AtomicStampedReference / AtomicMarkableReference (ABA protection)
- VarHandle (Java 9+, finer-grained alternative)
Implementation
Each method is a single atomic operation, no lock involved. compareAndSet returns true if the old value matched and the new value was installed. The retry loop pattern is the building block for any atomic compound operation.
1 import java.util.concurrent.atomic.AtomicInteger;
2
3 AtomicInteger counter = new AtomicInteger(0);
4
5 counter.incrementAndGet(); // ++ as one op, returns new
6 counter.getAndIncrement(); // ++ but returns old
7 counter.addAndGet(5);
8 counter.set(42); // volatile write
9 int v = counter.get(); // volatile read
10
11 // Custom compound operation: atomic max
12 void recordMax(int sample) {
13 while (true) {
14 int current = counter.get();
15 if (sample <= current) return;
16 if (counter.compareAndSet(current, sample)) return;
17 // CAS failed; another thread won; retry
18 }
19 }Many threads incrementing the same AtomicLong fight for the same cache line, and CAS keeps failing under contention. LongAdder shards the counter across many cells (one per thread, padded to avoid false sharing). Writes are nearly contention-free; sum() walks all cells.
1 import java.util.concurrent.atomic.LongAdder;
2
3 LongAdder requests = new LongAdder();
4
5 // Many threads:
6 requests.increment(); // cheap, sharded
7
8 // Periodically (e.g. every 10s):
9 long total = requests.sum(); // O(num cells)
10
11 // When to prefer AtomicLong: read-heavy workload (every request reads the value).
12 // When to prefer LongAdder: write-heavy, read-rarely (metrics, counters, tracing).Java 8+ added higher-order operations on atomics. They wrap the CAS retry loop. Cleaner than a hand-rolled loop and just as fast.
1 AtomicReference<List<String>> snapshot = new AtomicReference<>(List.of());
2
3 // Atomic add to an immutable list
4 void addItem(String item) {
5 snapshot.updateAndGet(current -> {
6 List<String> next = new ArrayList<>(current);
7 next.add(item);
8 return List.copyOf(next); // new immutable copy
9 });
10 }The ABA problem hits lock-free linked structures: thread A reads a node, B pops and re-pushes the same address with different contents, A's CAS still succeeds. AtomicStampedReference adds a version counter so the CAS only succeeds if both reference and version match.
1 import java.util.concurrent.atomic.AtomicStampedReference;
2
3 AtomicStampedReference<Node> head = new AtomicStampedReference<>(null, 0);
4
5 Node pop() {
6 int[] stamp = new int[1];
7 while (true) {
8 Node old = head.get(stamp);
9 if (old == null) return null;
10 Node next = old.next;
11 if (head.compareAndSet(old, next, stamp[0], stamp[0] + 1)) {
12 return old;
13 }
14 }
15 }Key points
- •All atomic classes provide volatile-style visibility plus atomic compound operations like incrementAndGet, compareAndSet, accumulateAndGet.
- •Use AtomicLong for counters with low contention. Use LongAdder when many threads write the same counter and reads are rare.
- •compareAndSet is the building block. Most other methods are CAS retry loops.
- •AtomicReference.compareAndSet protects against the wrong WRITE, but not against the wrong READ. Reading a reference and dereferencing it does not interlock with concurrent writes.
- •AtomicStampedReference adds a version counter to defeat the ABA problem.
Follow-up questions
▸AtomicLong vs LongAdder, how to choose?
▸Is compareAndSet really lock-free?
▸What is VarHandle and when is it needed?
▸Why is AtomicReference not enough for safe lazy init?
Gotchas
- !AtomicLong.get() then .set() is two operations, not one. Use accumulateAndGet or compareAndSet.
- !AtomicReferenceArray protects per-element ordering; a volatile array reference does NOT.
- !Under heavy contention, CAS retry loops can spin a lot. Move to LongAdder or sharded counters.
- !Compound operations across multiple atomics need a lock; atomics are single-variable only.
- !lazySet on Atomic* skips the release barrier. Faster, but no happens-before. Use only when the implications are well understood.