Java Atomic Variables
Lock-free thread-safe operations in Java with java.util.concurrent.atomic classes — counters, references, and compare-and-set.
Java Atomic Variables
volatile makes a single read or single write thread-safe. It can't make counter++ thread-safe — that's three operations. The java.util.concurrent.atomic package fills the gap. Its classes wrap a single value and expose operations like increment-and-get and compare-and-set as single atomic instructions — no lock, no synchronized block, just a CPU primitive (compare-and-swap, or CAS) that the JVM compiles down to.
Atomics are the right tool for a surprisingly large number of multithreaded patterns: counters, sequence numbers, flag-like state, and any "publish a new immutable snapshot" idiom. They're faster than synchronized under contention and dramatically simpler than hand-rolling a lock around a single field.
The family
The package has eight commonly used classes:
| Class | Wraps | Common operations |
|---|---|---|
AtomicInteger | int | get, set, incrementAndGet, addAndGet, compareAndSet |
AtomicLong | long | same as above, on long |
AtomicBoolean | boolean | get, set, compareAndSet |
AtomicReference<V> | V (any object reference) | get, set, compareAndSet, updateAndGet |
AtomicIntegerArray | int[] | per-index atomic ops |
AtomicLongArray | long[] | per-index atomic ops |
AtomicReferenceArray<V> | V[] | per-index atomic ops |
LongAdder / LongAccumulator | long | high-contention counter |
The first four are what you'll reach for 99% of the time.
AtomicInteger — the right counter
The replacement for "volatile int plus ++":
AtomicInteger counter = new AtomicInteger(); // starts at 0
counter.incrementAndGet(); // ++counter, atomic
counter.getAndIncrement(); // counter++, atomic
counter.addAndGet(5); // counter += 5, atomic
counter.set(42); // counter = 42, atomic
int n = counter.get(); // read
counter.compareAndSet(42, 100); // if (counter == 42) counter = 100; return whether it changedincrementAndGet is what you want for a simple counter; under the hood it's a CAS loop that the CPU does in one instruction on modern x86 (LOCK XADD). The whole operation is a single bus-level memory transaction — much cheaper than acquiring even an uncontended synchronized lock.
compareAndSet(expected, new) is the building block for almost everything else. It atomically writes new only if the current value is expected, and returns whether the write happened. With it you can build any single-field atomic update:
AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);
void recordMax(int v) {
int cur;
do {
cur = max.get();
if (v <= cur) return; // nothing to do
} while (!max.compareAndSet(cur, v)); // retry if someone else updated
}The CAS loop is the standard pattern: read, compute, try-to-write, retry on conflict. It's how incrementAndGet is implemented; it's how you'd write any compound update on a single field.
Java 8 simplified the loop:
max.updateAndGet(cur -> Math.max(cur, v)); // CAS loop hiddenupdateAndGet, accumulateAndGet, and getAndUpdate take a function and do the CAS loop for you. Prefer them when they fit.
AtomicReference<V> — the right way to swap an object
When the shared state is more than a primitive — a configuration map, a cached snapshot, an immutable holder — AtomicReference lets you atomically swap the whole object:
AtomicReference<Config> currentConfig = new AtomicReference<>(initialConfig);
void reload() {
Config c = readConfigFromDisk(); // expensive, lock-free
currentConfig.set(c); // publish atomically
}
Config get() { return currentConfig.get(); }The trick: the contents of Config must be immutable (or not touched after publish). The atomic swap publishes a finished value; if other threads then mutate the value's internals you've lost the safety. This is the immutable snapshot pattern, and it's the way most concurrent caches, route tables, and "global config" objects are built.
updateAndGet on a reference is also extremely useful:
AtomicReference<List<String>> log = new AtomicReference<>(List.of());
void append(String line) {
log.updateAndGet(old -> {
var copy = new ArrayList<>(old);
copy.add(line);
return List.copyOf(copy); // immutable snapshot
});
}Every reader gets a consistent immutable list. Writers race; the CAS loop retries the few that lose the race. Cheap under low contention, slow but correct under high.
LongAdder — the high-contention counter
Under heavy contention, AtomicLong.incrementAndGet becomes a bottleneck — every thread is hammering the same memory address, and the CPU has to serialise the bus transactions. LongAdder solves this by maintaining several internal counters, one per CPU, and summing them on read:
LongAdder requestCount = new LongAdder();
void onRequest() { requestCount.increment(); } // append-only, no contention
long snapshot() { return requestCount.sum(); } // sums every cell — not atomic but eventually consistentUse LongAdder when:
- The counter is incremented from many threads concurrently (think: per-request metrics in a web server).
- You read it rarely (every few seconds for a dashboard).
Use AtomicLong when:
- Incrementing is rare or single-threaded.
- You need an instantaneous accurate read.
LongAdder is one of the fastest concurrent counters anywhere — but the trade is that sum() is not atomic with concurrent increments. For the typical metrics-reporting case, that's fine.
What atomics are not
Atomics scale to one field. They don't compose across multiple fields:
AtomicInteger a = new AtomicInteger();
AtomicInteger b = new AtomicInteger();
a.incrementAndGet(); // atomic on its own
b.incrementAndGet(); // atomic on its own
// but the pair is NOT atomic — another thread can see new a, old bIf your invariant spans multiple fields ("a == b + 1 always"), you need a lock (or a single atomic on a holder object containing both).
Atomics also don't help with visibility of unrelated fields. Writing to an atomic doesn't publish other fields the way volatile does. Make those other fields volatile (or final, or write them through the atomic).
compareAndExchange and the new API (Java 9+)
Java 9 added compareAndExchange (returns the current value, not just a boolean):
int prev = counter.compareAndExchange(expected, newVal);
if (prev == expected) { // we won
...
} else { // somebody else got there first
// prev is the actual current value
}Java 9 also added the VarHandle API which exposes weak CAS, ordered access, etc., for low-level concurrent libraries. You very rarely need it; mention it here so you've seen the name.
A worked example: counter and snapshot
The program below contrasts four counters: unsynchronised, volatile, AtomicInteger, and LongAdder. All four are hit by 8 threads doing 100,000 increments each.
What to take from the run:
plainandvolatileboth lost updates — sometimes spectacularly (a final count far below the expected800,000).volatilefixes the visibility problem butn++is still three operations. This is the most important thing to remember aboutvolatile: it does not make compound updates atomic.AtomicIntegerproduced the exact expected count, every run. The per-increment cost was a few nanoseconds — significantly higher thann++on a plainint(which is one or two), but no lock acquisitions and no thread blocking. Under contention it's faster thansynchronizedby a wide margin.LongAdderwas the fastest counter under the 8-thread load — it scatters writes across separate cells per CPU so the threads don't contend on a single cache line. The trade issum()is not atomic withincrement()(a reader can see a slightly stale total), which is exactly the right deal for metrics and counters where instantaneous accuracy doesn't matter.- The CAS-loop max recorded the largest value seen across all samples. The loop is the general pattern: read the current value, compute the desired new value, try to write it; if somebody else wrote first, the CAS fails and you retry. Most
updateAndGetandaccumulateAndGetcalls are this loop with the boilerplate hidden. - The
AtomicReference<List<String>>produced an immutable snapshot of the log. Each writer built a new immutable copy and tried to publish it; under contention, two writers might both build a copy and one's CAS fails — that thread retries, reads the freshly-updated list, and merges. The pattern is wasteful under heavy contention (lots of throw-away copies) but ideal for "read-heavy, occasionally-rebuilt" snapshots.
What's next
The next chapter, Java Locks, starts the java.util.concurrent.locks story — the Lock interface, why it exists alongside synchronized, and the capabilities (tryLock, lockInterruptibly, Condition) it adds that the intrinsic monitor doesn't have.
Practice
Which of these is the correct way to safely increment a shared counter from many threads in a tight loop?