The Problem with Locks
Locks work, but they have costs:
- Contention — threads waiting for a lock do nothing useful.
- Context switches — the OS moves blocked threads on and off the CPU.
- Priority inversion — a low-priority thread holding a lock blocks a high-priority thread.
- Deadlock risk — multiple locks can create circular dependencies.
For simple operations — incrementing a counter, updating a reference — there’s a better way.
Compare-And-Swap (CAS)
CAS is the foundation of all lock-free programming in Java. It’s a single CPU instruction that atomically:
- Reads the current value at a memory location
- Compares it to an expected value
- If they match, writes a new value
- Returns whether the swap succeeded

In pseudocode:
boolean CAS(memoryLocation, expectedValue, newValue):
if memoryLocation.value == expectedValue:
memoryLocation.value = newValue
return true
else:
return false
This entire operation is a single atomic CPU instruction (cmpxchg on x86, ldxr/stxr on ARM). No lock is needed because the hardware guarantees atomicity.
The java.util.concurrent.atomic Package
Java wraps CAS operations in a set of atomic classes:
Scalar Types
| Class | Wraps | Key Use |
|---|---|---|
AtomicBoolean | boolean | Flags, one-shot triggers |
AtomicInteger | int | Counters, accumulators |
AtomicLong | long | Counters, sequence numbers |
AtomicReference<T> | Object reference | Lock-free data structures |
Array Types
| Class | Wraps |
|---|---|
AtomicIntegerArray | int[] — each element is individually atomic |
AtomicLongArray | long[] |
AtomicReferenceArray<T> | T[] |
Stamped/Marked References
| Class | Purpose |
|---|---|
AtomicMarkableReference<T> | Reference + boolean mark (for soft-delete flags) |
AtomicStampedReference<T> | Reference + int stamp (solves ABA problem) |
Accumulators (Java 8+)
| Class | Purpose |
|---|---|
LongAdder | High-throughput counter (better than AtomicLong under contention) |
LongAccumulator | Generalized accumulation with custom function |
DoubleAdder | High-throughput double accumulator |
DoubleAccumulator | Generalized double accumulation |
AtomicInteger — Key Methods
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
| Method | Description | Return |
|---|---|---|
get() | Read current value | Current value |
set(int value) | Set value (volatile write) | void |
getAndSet(int value) | Set and return previous | Previous value |
compareAndSet(int expect, int update) | CAS operation | true if successful |
getAndIncrement() | Post-increment (i++) | Previous value |
incrementAndGet() | Pre-increment (++i) | New value |
getAndDecrement() | Post-decrement (i–) | Previous value |
decrementAndGet() | Pre-decrement (–i) | New value |
getAndAdd(int delta) | Add and return previous | Previous value |
addAndGet(int delta) | Add and return new | New value |
getAndUpdate(IntUnaryOperator) | Apply function, return previous | Previous value |
updateAndGet(IntUnaryOperator) | Apply function, return new | New value |
getAndAccumulate(int x, IntBinaryOperator) | Accumulate with value | Previous value |
accumulateAndGet(int x, IntBinaryOperator) | Accumulate with value | New value |
Usage Examples
AtomicInteger counter = new AtomicInteger(0);
// Simple increment
counter.incrementAndGet(); // 1
// Conditional update
boolean success = counter.compareAndSet(1, 10); // true, counter is now 10
// Custom update function
counter.updateAndGet(current -> current * 2); // 20
// Accumulate
counter.accumulateAndGet(5, Integer::max); // max(20, 5) = 20
counter.accumulateAndGet(50, Integer::max); // max(20, 50) = 50
AtomicReference — Key Methods
import java.util.concurrent.atomic.AtomicReference;
AtomicReference<String> ref = new AtomicReference<>("initial");
| Method | Description |
|---|---|
get() | Read current reference |
set(V value) | Set reference (volatile write) |
getAndSet(V value) | Swap and return previous |
compareAndSet(V expect, V update) | CAS on reference identity |
getAndUpdate(UnaryOperator<V>) | Apply function, return previous |
updateAndGet(UnaryOperator<V>) | Apply function, return new |
Important: CAS Compares Identity, Not Equality
AtomicReference<String> ref = new AtomicReference<>("hello");
String current = ref.get();
// CAS uses == (reference identity), not .equals()
ref.compareAndSet(current, "world"); // Works — same reference
ref.compareAndSet(new String("world"), "!"); // FAILS — different object, even if .equals() is true
LongAdder vs AtomicLong
Under high contention (many threads hammering the same counter), AtomicLong degrades because every failed CAS triggers a retry. LongAdder solves this by spreading updates across multiple internal cells:
import java.util.concurrent.atomic.LongAdder;
LongAdder adder = new LongAdder();
// Multiple threads call:
adder.increment(); // Spreads across internal cells
adder.add(5);
// When you need the total:
long total = adder.sum(); // Aggregates all cells
Performance Comparison
| Scenario | AtomicLong | LongAdder |
|---|---|---|
| Low contention (few threads) | Fast | Same (slight overhead) |
| High contention (many threads) | Degrades — CAS retries | Scales linearly |
| Reading the value | O(1) | O(cells) — must sum |
| Memory | Fixed | Grows with contention |
Rule of thumb: Use AtomicLong when you read the value frequently. Use LongAdder when updates are frequent and reads are rare (like metrics/counters).
AtomicStampedReference — Solving the ABA Problem
The ABA Problem
CAS checks: “Is the value still what I expect?” But what if the value was A, changed to B, and changed back to A? CAS sees A and thinks nothing happened — but the intermediate state change might matter.
Thread 1: reads A
Thread 2: changes A → B → A
Thread 1: CAS(expected=A, new=C) → succeeds! (but state was modified)
Solution: Stamp the Reference
import java.util.concurrent.atomic.AtomicStampedReference;
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
// Read value AND stamp
int[] stampHolder = new int[1];
String current = ref.get(stampHolder);
int currentStamp = stampHolder[0];
// CAS checks BOTH reference AND stamp
boolean success = ref.compareAndSet(
current, // expected reference
"B", // new reference
currentStamp, // expected stamp
currentStamp + 1 // new stamp
);
Even if the reference returns to the same value, the stamp will be different, and CAS will correctly detect the change.
Building Thread-Safe Patterns with Atomics
Lock-Free Counter
public class MetricsCounter {
private final AtomicLong requestCount = new AtomicLong(0);
private final AtomicLong errorCount = new AtomicLong(0);
private final LongAdder totalLatency = new LongAdder();
public void recordRequest(long latencyMs, boolean isError) {
requestCount.incrementAndGet();
totalLatency.add(latencyMs);
if (isError) {
errorCount.incrementAndGet();
}
}
public double getErrorRate() {
long requests = requestCount.get();
return requests == 0 ? 0 : (double) errorCount.get() / requests;
}
public double getAverageLatency() {
long requests = requestCount.get();
return requests == 0 ? 0 : (double) totalLatency.sum() / requests;
}
}
Lock-Free Lazy Initialization
public class AtomicLazyInit<T> {
private final AtomicReference<T> ref = new AtomicReference<>();
private final Supplier<T> factory;
public AtomicLazyInit(Supplier<T> factory) {
this.factory = factory;
}
public T get() {
T instance = ref.get();
if (instance != null) {
return instance;
}
T newInstance = factory.get();
if (ref.compareAndSet(null, newInstance)) {
return newInstance; // We won the race
} else {
return ref.get(); // Another thread initialized first
}
}
}
Lock-Free Maximum Tracker
public class MaxTracker {
private final AtomicLong max = new AtomicLong(Long.MIN_VALUE);
public void observe(long value) {
long current;
do {
current = max.get();
if (value <= current) return; // Not a new max
} while (!max.compareAndSet(current, value));
}
public long getMax() {
return max.get();
}
}
When to Use Atomic Classes
| Use Case | Recommended Class |
|---|---|
| Simple counter | AtomicInteger / AtomicLong |
| High-contention counter | LongAdder |
| Boolean flag | AtomicBoolean |
| Shared mutable reference | AtomicReference<T> |
| Lock-free data structures | AtomicReference<T> + CAS loop |
| Counter per array index | AtomicIntegerArray |
| ABA-sensitive algorithms | AtomicStampedReference<T> |
| Soft-delete markers | AtomicMarkableReference<T> |
| Custom accumulation | LongAccumulator |
Summary
- Atomic classes use CAS (Compare-And-Swap) — a single CPU instruction — instead of locks.
compareAndSetis the core operation: “update only if the value is what I expect.”- Under low contention, atomics are faster than locks. Under high contention,
LongAdderscales better thanAtomicLong. - CAS loops (read → compute → CAS → retry if failed) are the standard pattern for lock-free updates.
- The ABA problem is solved by
AtomicStampedReferencewhich checks both value and version. - Atomics provide visibility guarantees equivalent to
volatile.