Atomic Classes: AtomicInteger, AtomicReference and CAS
volatile guarantees visibility but not atomicity: counter++ on a volatile int is still a broken read-modify-write. The java.util.concurrent.atomic package provides thread-safe scalars and references using hardware Compare-And-Set (CAS) instructions: no locks, no blocking, and far less overhead than synchronized for single-variable operations.
Contents
Why Atomic Classes (volatile is Not Enough)
volatilevolatilecounter++java.util.concurrent.atomic
// volatile guarantees that every thread sees the latest value,
// but it does NOT make compound operations atomic.
class VolatileBroken {
private volatile int counter = 0;
// counter++ is THREE operations: READ, INCREMENT, WRITE
// Two threads can both read 5, both increment to 6, both write 6 — lost update!
void increment() {
counter++; // NOT atomic even with volatile
}
}
// synchronized fixes it but adds lock overhead and can block threads:
class SynchronizedCounter {
private int counter = 0;
synchronized void increment() { counter++; }
synchronized int get() { return counter; }
}
// AtomicInteger fixes it without locks — uses hardware CAS instruction:
class AtomicCounter {
private final AtomicInteger counter = new AtomicInteger(0);
void increment() { counter.incrementAndGet(); } // atomic, no lock
int get() { return counter.get(); } // always consistent
}
// CAS (Compare-And-Set) hardware instruction:
// "If the current value equals expectedValue, set it to newValue atomically."
// If another thread changed the value first, CAS fails — retry with new value.
// This is how all Atomic* classes work under the hood.
// Performance rule of thumb:
// Low contention → AtomicInteger ≈ synchronized (both fast)
// High contention → AtomicInteger >> synchronized (no blocking)
// Very high contention → LongAdder >> AtomicInteger (see LongAdder section)
CMPXCHGLDXR/STXRAtomicInteger: Core Methods
AtomicIntegerintgetAndX()xAndGet()i++++igetAndUpdate()updateAndGet()UnaryOperatorcompareAndSet(expected, update)trueexpected
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger ai = new AtomicInteger(0);
// --- Basic get/set ---
int val = ai.get(); // read current value
ai.set(10); // unconditional write (still volatile-visible)
ai.lazySet(10); // relaxed write — eventually visible (faster, rare use)
// --- Increment / Decrement ---
int prev = ai.getAndIncrement(); // returns OLD value, then increments (like i++)
int next = ai.incrementAndGet(); // increments, returns NEW value (like ++i)
int prev2 = ai.getAndDecrement(); // returns OLD, then decrements
int next2 = ai.decrementAndGet(); // decrements, returns NEW
// --- Add ---
int prev3 = ai.getAndAdd(5); // returns OLD, then adds 5
int next3 = ai.addAndGet(5); // adds 5, returns NEW
// --- Update with a function (Java 8+) ---
// getAndUpdate — apply UnaryOperator, return old value
int old = ai.getAndUpdate(x -> x * 2);
// updateAndGet — apply UnaryOperator, return new value
int newVal = ai.updateAndGet(x -> x * 2);
// accumulateAndGet — combine current value with given value using BinaryOperator
int result = ai.accumulateAndGet(3, Integer::max); // atomically: value = max(value, 3)
// --- compareAndSet (CAS) ---
boolean swapped = ai.compareAndSet(10, 20);
// If current value == 10, set to 20 and return true; else return false
// Manual CAS loop (pattern used internally by all getAndUpdate methods):
int prev4, next4;
do {
prev4 = ai.get();
next4 = prev4 + 5; // compute new value
} while (!ai.compareAndSet(prev4, next4)); // retry if another thread changed it
// Real-world example: thread-safe request counter
class RequestTracker {
private final AtomicInteger total = new AtomicInteger(0);
private final AtomicInteger errors = new AtomicInteger(0);
void recordSuccess() { total.incrementAndGet(); }
void recordError() { total.incrementAndGet(); errors.incrementAndGet(); }
int getTotal() { return total.get(); }
int getErrors() { return errors.get(); }
double errorRate() {
int t = total.get();
return t == 0 ? 0.0 : (double) errors.get() / t;
}
}
AtomicLong and AtomicBoolean
AtomicLongAtomicIntegerlongAtomicBooleancompareAndSet(false, true)
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicBoolean;
// --- AtomicLong — identical API to AtomicInteger, but for long ---
AtomicLong sequence = new AtomicLong(0);
// Distributed ID generator — safe across threads
long nextId() {
return sequence.incrementAndGet();
}
// Timestamp tracking — keep the maximum seen timestamp
AtomicLong maxTimestamp = new AtomicLong(0);
void observeTimestamp(long ts) {
// atomically update to max without a lock:
maxTimestamp.accumulateAndGet(ts, Math::max);
}
// Byte counter for I/O statistics
class IoStats {
private final AtomicLong bytesRead = new AtomicLong(0);
private final AtomicLong bytesWritten = new AtomicLong(0);
void addRead(long n) { bytesRead.addAndGet(n); }
void addWritten(long n) { bytesWritten.addAndGet(n); }
long totalRead() { return bytesRead.get(); }
long totalWritten() { return bytesWritten.get(); }
}
// --- AtomicBoolean — one-shot flag, init guard ---
AtomicBoolean started = new AtomicBoolean(false);
// Only the FIRST thread to call this will run initSystem(); rest are skipped.
void initOnce() {
if (started.compareAndSet(false, true)) { // CAS: only succeeds once
initSystem();
}
}
// Shutdown hook — ensure shutdown runs exactly once even if multiple threads call it
class Service {
private final AtomicBoolean stopped = new AtomicBoolean(false);
void stop() {
if (stopped.compareAndSet(false, true)) {
releaseResources(); // called exactly once
}
}
}
// AtomicBoolean is NOT just a boolean wrapper — its compareAndSet is the key:
// compareAndSet(false, true) is an atomic "test and set" — used for one-shot guards
AtomicReference and CAS
AtomicReference<V>compareAndSet==equals()AtomicReferenceAtomicStampedReference
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
// AtomicReference<V> — same CAS semantics for object references
AtomicReference<String> current = new AtomicReference<>("initial");
current.set("updated");
String old = current.getAndSet("final"); // returns "updated", sets "final"
// CAS on a reference — compare by identity (==), not equals()
boolean ok = current.compareAndSet("final", "reset"); // true if current == "final"
// Immutable config hot-reload pattern:
record AppConfig(String host, int port, int timeout) {}
class ConfigHolder {
private final AtomicReference<AppConfig> config =
new AtomicReference<>(new AppConfig("localhost", 8080, 30));
AppConfig get() { return config.get(); }
// Safe reload — readers always see a consistent config snapshot
void reload(AppConfig newConfig) {
config.set(newConfig); // atomic reference swap — no partial state visible
}
}
// ABA problem — CAS sees value A, misses A→B→A change, sets incorrectly
// Example: stack push/pop with AtomicReference<Node> can corrupt if Node is reused.
// Fix: AtomicStampedReference — attach a version stamp to detect ABA
AtomicStampedReference<String> stamped =
new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String val = stamped.get(stampHolder); // reads value AND current stamp
int stamp = stampHolder[0];
// CAS only succeeds if BOTH value and stamp match:
boolean swapped = stamped.compareAndSet("A", "B", stamp, stamp + 1);
// Lock-free stack using AtomicReference:
class LockFreeStack<T> {
private static class Node<T> {
final T item;
Node<T> next;
Node(T item, Node<T> next) { this.item = item; this.next = next; }
}
private final AtomicReference<Node<T>> top = new AtomicReference<>();
void push(T item) {
Node<T> newHead;
Node<T> oldHead;
do {
oldHead = top.get();
newHead = new Node<>(item, oldHead);
} while (!top.compareAndSet(oldHead, newHead)); // retry if another push raced
}
T pop() {
Node<T> oldHead;
Node<T> newHead;
do {
oldHead = top.get();
if (oldHead == null) return null; // empty
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
}
AtomicStampedReferenceAtomicMarkableReferenceAtomicIntegerArray
AtomicIntegerArrayAtomicLongArrayAtomicReferenceArraylong
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicReferenceArray;
// AtomicIntegerArray — array where each element can be updated atomically
int[] initialValues = {1, 2, 3, 4, 5};
AtomicIntegerArray arr = new AtomicIntegerArray(initialValues);
// Note: a copy is made — modifying initialValues after this has no effect
// Element-level atomic operations:
int v = arr.get(0); // read element 0
arr.set(0, 10); // write element 0
int prev = arr.getAndIncrement(2); // atomically increment element 2
arr.addAndGet(3, 5); // atomically add 5 to element 3
boolean ok = arr.compareAndSet(4, 5, 99); // CAS element 4: if 5, set to 99
int len = arr.length(); // array length
// Use case: per-bucket counters (sharded counter / histogram)
class Histogram {
private static final int BUCKETS = 100;
private final AtomicIntegerArray buckets = new AtomicIntegerArray(BUCKETS);
void record(int value) {
int bucket = Math.min(value / 10, BUCKETS - 1);
buckets.incrementAndGet(bucket);
}
int count(int bucket) { return buckets.get(bucket); }
void print() {
for (int i = 0; i < BUCKETS; i++) {
System.out.printf("Bucket %3d: %d%n", i * 10, buckets.get(i));
}
}
}
// AtomicLongArray and AtomicReferenceArray have the same API for longs and objects:
AtomicLongArray timestamps = new AtomicLongArray(10);
AtomicReferenceArray<String> names = new AtomicReferenceArray<>(10);
timestamps.set(0, System.currentTimeMillis());
names.compareAndSet(0, null, "Alice"); // set element 0 from null to "Alice"
LongAdder for High-Contention Counters
Under heavy contention, many threads hammering a single AtomicLongLongAddersum()sum()AtomicLong
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAccumulator;
// Problem with AtomicLong under heavy contention:
// All threads compete on ONE memory location — CAS failures cause retries → CPU waste
// LongAdder solution: each thread updates its OWN cell; sum() adds them up
// Under low contention: behaves like AtomicLong (one base cell)
// Under high contention: expands to per-thread cells automatically
LongAdder counter = new LongAdder();
// Producer threads:
counter.increment(); // increment by 1
counter.add(5); // add arbitrary delta
counter.decrement(); // subtract 1
// Consumer reads the sum:
long total = counter.sum(); // adds all cells — NOT atomic with increments
counter.reset(); // reset to 0 (not atomic — use carefully)
long sumAndReset = counter.sumThenReset(); // sum then reset atomically-ish
// Benchmark insight:
// 16 threads doing 10M increments each:
// synchronized int ~3.5 s
// AtomicLong ~1.2 s
// LongAdder ~0.3 s (fastest for pure counting)
// LongAccumulator — generalized LongAdder with a custom binary operator
LongAccumulator maxVal = new LongAccumulator(Long::max, Long.MIN_VALUE);
maxVal.accumulate(42);
maxVal.accumulate(17);
maxVal.accumulate(99);
System.out.println(maxVal.get()); // 99 — thread-safe running maximum
// DoubleAdder — same idea for doubles (floating-point summation)
DoubleAdder total2 = new DoubleAdder();
total2.add(1.5);
total2.add(2.3);
System.out.println(total2.sum()); // 3.8
// When NOT to use LongAdder:
// - sum() is NOT linearizable with add() — reading mid-increment may be inaccurate
// - If you need CAS semantics (compareAndSet) → use AtomicLong
// - LongAdder uses more memory than AtomicLong (array of cells)
LongAdderAtomicLongLock-Free Counter Example
A concrete benchmark is the clearest way to understand when each counter type pays off. All three implementations (synchronizedAtomicIntegerLongAddersynchronizedAtomicIntegerLongAdderlong
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.*;
// Full example: comparing synchronized, AtomicInteger, and LongAdder counters
// under concurrent load from multiple threads.
// 1. Synchronized (baseline)
class SyncCounter {
private long count = 0;
synchronized void inc() { count++; }
synchronized long get() { return count; }
}
// 2. AtomicInteger (lock-free)
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
void inc() { count.incrementAndGet(); }
long get() { return count.get(); }
}
// 3. LongAdder (best for write-heavy)
class AdderCounter {
private final LongAdder count = new LongAdder();
void inc() { count.increment(); }
long get() { return count.sum(); }
}
// Driver — 8 threads, 1 million increments each:
public class LockFreeDemo {
static final int THREADS = 8;
static final int OPS_PER_THREAD = 1_000_000;
static long benchmark(Runnable inc, java.util.function.LongSupplier get)
throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
long start = System.nanoTime();
CountDownLatch latch = new CountDownLatch(THREADS);
for (int i = 0; i < THREADS; i++) {
pool.submit(() -> {
for (int j = 0; j < OPS_PER_THREAD; j++) inc.run();
latch.countDown();
});
}
latch.await();
long elapsed = System.nanoTime() - start;
pool.shutdown();
long expected = (long) THREADS * OPS_PER_THREAD;
assert get.getAsLong() == expected : "Counter is wrong!";
return elapsed / 1_000_000; // ms
}
public static void main(String[] args) throws Exception {
SyncCounter sync = new SyncCounter();
AtomicCounter atomic = new AtomicCounter();
AdderCounter adder = new AdderCounter();
System.out.println("Synchronized: " + benchmark(sync::inc, sync::get) + " ms");
System.out.println("AtomicInteger: " + benchmark(atomic::inc, atomic::get) + " ms");
System.out.println("LongAdder: " + benchmark(adder::inc, adder::get) + " ms");
// Typical output on 8-core machine:
// Synchronized: 620 ms
// AtomicInteger: 210 ms
// LongAdder: 55 ms
}
}
Note: All three implementations produce the correct final count: the difference is throughput. LongAdderAtomicInteger