What are Atomic classes
Unlike locks (synchronized, ReentrantLock), which suspend contending threads into BLOCKED or WAITING states via kernel calls, atomic classes rely on two core pillars:
🟢 Junior Level
30-Second Summary
Atomic classes in the java.util.concurrent.atomic package provide lock-free, thread-safe operations on individual variables and object references without thread blocking or OS-level context switching.
Unlike locks (synchronized, ReentrantLock), which suspend contending threads into BLOCKED or WAITING states via kernel calls, atomic classes rely on two core pillars:
volatilememory semantics: Guarantees immediate cross-thread visibility and prevents instruction reordering (JMM Happens-Before).- Hardware CAS (
Compare-And-Swap) instructions: CPU-level atomic primitives that verify and update a memory location in a single indivisible transaction.
If another thread modifies the variable during an update, the operation does not block; instead, it spins in an optimistic CAS retry loop until the atomic update succeeds.
The Compound Operation Problem (count++)
A standard increment operation count++ is not atomic. At the bytecode and machine instruction levels, it breaks down into three distinct steps:
- Read: Load the current value from main memory/cache into a CPU register.
- Modify: Increment the value inside the CPU register.
- Write: Store the modified value back to cache and main memory.
When executed concurrently by multiple threads, an interleaved sequence of reads and writes triggers a Race Condition, resulting in lost updates:
// ❌ UNSAFE: Lost updates under concurrent access
private int unsafeCount = 0;
public void addUnsafe() { unsafeCount++; }
// ✅ THREAD-SAFE & LOCK-FREE: Indivisible CPU CAS primitive
private final AtomicInteger safeCount = new AtomicInteger(0);
public void addSafe() { safeCount.incrementAndGet(); }
Taxonomy of Atomic Classes in Java
| Category | Classes | Purpose & Typical Use Case |
|---|---|---|
| Scalar Primitives | AtomicBoolean, AtomicInteger, AtomicLong |
Atomic sequence counters, metrics, state flags |
| Reference Types | AtomicReference<V> |
Atomic reference swaps of immutable domain models |
| Versioned References | AtomicStampedReference<V>, AtomicMarkableReference<V> |
Solves the ABA problem by pairing references with integer stamps or boolean marks |
| Atomic Arrays | AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray<E> |
Per-element volatile access and CAS operations |
| Field Updaters | AtomicIntegerFieldUpdater<T>, AtomicReferenceFieldUpdater<T,V> |
Reflection-based CAS over plain volatile fields to minimize memory overhead |
| Scalable Accumulators | LongAdder, LongAccumulator, DoubleAdder |
High-throughput striped counters under heavy thread contention |
Idiomatic Java 21 Example
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicDemo {
// 1. Single-execution initialization flag
private final AtomicBoolean initialized = new AtomicBoolean(false);
// 2. High-speed atomic counter
private final AtomicInteger requestCounter = new AtomicInteger(0);
// 3. Atomic publication of an immutable record
public record AppConfig(String host, int port) {}
private final AtomicReference<AppConfig> config =
new AtomicReference<>(new AppConfig("localhost", 8080));
public void initOnce() {
// Guaranteed to execute exactly once across all concurrent threads
if (initialized.compareAndSet(false, true)) {
System.out.println("Subsystem successfully initialized");
}
}
public void handleRequest() {
requestCounter.incrementAndGet(); // Thread-safe atomic increment
}
public void updateConfig(String newHost, int newPort) {
// Functional, lock-free update using an optimistic CAS loop
config.updateAndGet(old -> new AppConfig(newHost, newPort));
}
}
🟡 Middle Level
CAS Mechanics & The Optimistic Retry Loop
The CAS operation accepts three parameters:
- Memory offset / address of the variable ($V$).
- Expected old value ($A$).
- New target value ($B$).
Semantics: The value $B$ is written to memory address $V$ if and only if the current value at $V$ exactly matches $A$. The check and write are executed as a single atomic CPU cycle.
Optimistic Spin Loop:
If CAS returns false (indicating a competing thread updated the variable first), the calling thread does not enter kernel sleep. Instead, it re-reads the updated value and retries the CAS operation:
// Underlying mechanics of getAndAddInt:
public final int getAndAddInt(Object o, long offset, int delta) {
int current;
do {
current = getIntVolatile(o, offset); // 1. Volatile read of current state
} while (!weakCompareAndSetInt(o, offset, current, current + delta)); // 2. Optimistic CAS write
return current;
}
Architectural Evolution: Unsafe vs VarHandle (Java 9+)
Historically, atomic classes were implemented via sun.misc.Unsafe, a proprietary internal API operating on raw memory addresses.
Starting in Java 9 (JEP 193), atomic classes were re-architected on top of java.lang.invoke.VarHandle:
- Provides strongly typed, verified access to fields and array elements.
- Maps directly to JMM access modes without reflection performance penalties.
- Fully optimized by the HotSpot C2 JIT compiler into inline machine assembly.
Memory Access Modes in Modern Atomic Classes
Modern atomic classes expose fine-grained control over memory barriers:
AtomicInteger atom = new AtomicInteger(0);
// 1. Full Volatile Write: Enforces StoreStore + StoreLoad barriers
atom.set(42);
// 2. Release / Ordered Write: Generates StoreStore barrier, skips expensive StoreLoad
atom.lazySet(42);
// 3. Full Bi-directional Barrier: Acquire + Release semantics
atom.compareAndSet(42, 100);
// 4. Weak CAS: Allows spurious failures, avoids full fencing on certain architectures
atom.weakCompareAndSetVolatile(100, 200);
Why Use lazySet()?
lazySet() writes the value with Ordered / Release semantics. It guarantees that all preceding writes by the thread are flushed to memory before this write (via a StoreStore barrier), but it omits the expensive StoreLoad CPU barrier. Other threads will observe the value with a negligible hardware delay (eventual visibility). This provides a massive throughput boost in scenarios like ring buffers (e.g., LMAX Disruptor) or clearing references in lock-free queues.
🔴 Senior Level
Hardware-Level CAS: x86-64 vs. ARM64 / RISC-V
The physical implementation of CAS differs fundamentally between CPU architectures:
- x86-64 Architecture (CISC):
- Utilizes the single instruction
LOCK CMPXCHG. - The
LOCKprefix asserts exclusive ownership over the target CPU cache line via the cache coherency protocol (MESI / MOESI). The cache line is locked during the execution of the instruction, ensuring hardware-level atomicity.
- Utilizes the single instruction
- ARM64 and RISC-V Architectures (RISC):
- Do not provide a monolithic atomic CAS instruction.
- Employ a pair of instructions: LL/SC (Load-Linked / Store-Conditional):
LDXR(Load Exclusive): Reads the memory value and registers an exclusive hardware monitor on the cache line.STXR(Store Exclusive): Attempts to write the new value; succeeds only if no other processor core has invalidated or written to that monitored cache line since theLDXR.
The ABA Problem & Version Stamps
The ABA problem is an inherent vulnerability in lock-free data structures (such as Treiber lock-free stacks or Michael-Scott queues):
1. Thread 1 reads top node 'A' (which references 'B').
2. Thread 1 stalls before executing CAS.
3. Thread 2 pops 'A', pops 'B', and deallocates them.
4. Thread 2 allocates a new node that happens to be assigned memory address 'A', and pushes it to the stack.
5. Thread 1 resumes and executes CAS(A, B).
Memory address matches ('A' == 'A')!
CAS succeeds, but the stack is corrupted because 'B' points to invalid memory!
The Solution: AtomicStampedReference
AtomicStampedReference binds an object reference to an integer version stamp (revision number):
AtomicStampedReference<Node> stackTop = new AtomicStampedReference<>(initialNode, 0);
int[] stampHolder = new int[1];
Node currentTop = stackTop.get(stampHolder);
int currentStamp = stampHolder[0];
// CAS succeeds ONLY if BOTH the reference AND the version stamp match:
boolean success = stackTop.compareAndSet(
currentTop,
newTop,
currentStamp,
currentStamp + 1
);
High Contention Bottleneck: AtomicLong vs LongAdder
Under extreme write contention across dozens of threads (e.g., global metrics, rate limiters), AtomicLong suffers severe performance degradation:
- Every thread executes
LOCK CMPXCHGon the exact same 64-byte CPU cache line. - This triggers Cache Bouncing (Cache Line Ping-Pong): the cache line is continuously invalidated and transferred across CPU cores via the interconnect bus (QPI/UPI), saturating hardware bandwidth.
- Threads spin fruitlessly in CAS retry loops, burning 100% CPU.
Cache Line Bouncing in AtomicLong
CPU Core 1 CPU Core 2 CPU Core 3
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ LOCK CMPXCHG │ │ LOCK CMPXCHG │ │ LOCK CMPXCHG │
└───────┬──────┘ └───────┬──────┘ └───────┬──────┘
│ │ │
└────────────► [ Cache Line: value ] ◄────────────┘
(Continuous Cache Invalidation)
Architecture of LongAdder (Striped64):
LongAdder eliminates central memory contention through dynamic sharding:
- Under zero contention, threads update a single
volatile long basefield. - Upon the first CAS collision, it initializes an internal array:
volatile Cell[] cells. - Each thread hashes to a separate
Cellslot based on its thread-local probe (Thread.currentThread().threadLocalRandomProbe). - The
Cellclass is annotated with@jdk.internal.vm.annotation.Contended, which injects 128 bytes of padding around the field to eliminate False Sharing by guaranteeing each cell occupies its own CPU cache line. - Calling
sum()aggregatesbaseand all activeCellvalues on demand.
Memory Optimization: AtomicIntegerFieldUpdater
An AtomicInteger object requires 16 bytes (with Compressed OOPs) or 24 bytes of heap memory as a standalone object header and payload wrapper.
When an application tracks millions of domain objects (e.g., active socket sessions or in-memory game entities), allocating dedicated atomic wrappers consumes significant memory:
// ❌ Wasteful: 1,000,000 entities * 24 bytes = 24 MB heap overhead for wrapper objects
public class Session {
private final AtomicInteger state = new AtomicInteger(0);
}
// ✅ Optimized: Raw volatile primitive (4 bytes) + static singleton updater
public class OptimizedSession {
private static final AtomicIntegerFieldUpdater<OptimizedSession> STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(OptimizedSession.class, "state");
// Field MUST be declared volatile and cannot be private!
volatile int state = 0;
public void activate() {
STATE_UPDATER.compareAndSet(this, 0, 1);
}
}
4 Tricky Questions
1. What is the difference between set(), lazySet(), and compareAndSet() in AtomicInteger at the memory barrier level?
Answer:
set(v): Performs a full volatile write. The compiler/JVM emits aStoreStorebarrier prior to the store and a heavyweightStoreLoadbarrier immediately following it. This forces the CPU Store Buffer to drain, making the write visible to all other cores immediately.lazySet(v): Performs an ordered/release write. It emits aStoreStorebarrier (ensuring prior memory stores complete before this write) but omits theStoreLoadbarrier. The write executes with minimal CPU pipeline stalling, but cross-core visibility is slightly delayed (eventual consistency).compareAndSet(expect, update): Enforces a full bidirectional memory barrier (Acquire + Release). No preceding or succeeding memory operations can be reordered across the CAS boundary.
2. What is the ABA problem, in which scenarios is it critical, and is it dangerous for a simple AtomicInteger counter?
Answer:
The ABA problem occurs when a memory location originally holding value $A$ is changed to $B$ and then restored back to $A$. A thread executing CAS(A, C) perceives that the value has remained unchanged and succeeds, even though intermediate state transitions took place.
- Harmless for scalar counters: In a plain numeric counter (
AtomicInteger), the intermediate states do not invalidate the arithmetic invariant; the number $A$ is mathematically indistinguishable from another $A$. - Catastrophic in lock-free memory management: In lock-free node-based structures (e.g., memory reuse in Treiber stacks), if node $A$ is popped, freed, and re-allocated at the same memory address, its internal
nextpointer may have changed. A thread verifying only pointer address equality will link corrupt or obsolete memory structures. The solution isAtomicStampedReference, which pairs the pointer with a monotonically increasing integer revision stamp.
3. Why does calling LongAdder.sum() during active concurrent writes not guarantee an exact point-in-time snapshot?
Answer:
LongAdder.sum() does not acquire a global lock, nor does it freeze writer threads. It simply iterates through the base field and the Cell[] array, accumulating their values. If a concurrent thread increments a cell that sum() has already processed, or if a cell moves during internal re-hashing, the computed total will not represent an instantaneous global state snapshot. LongAdder is engineered specifically for high-throughput metrics collection where eventual accumulation speed takes priority over exact point-in-time snapshot consistency.
4. How can you atomically update two interrelated fields without locks (synchronized/Lock)?
Answer:
You cannot use two separate AtomicInteger variables; updating them sequentially creates a race window between the first and second updates.
The correct lock-free idiom:
- Encapsulate both fields inside an immutable data holder (in modern Java, a
recordis ideal). - Store the record reference inside an
AtomicReference<ImmutableState>. - Perform an atomic state swap using
updateAndGet(): ```java public record Coordinate(int x, int y) {} private final AtomicReferencepos = new AtomicReference<>(new Coordinate(0, 0));
public void move(int dx, int dy) { pos.updateAndGet(curr -> new Coordinate(curr.x() + dx, curr.y() + dy)); } ```
🎯 Interview Cheat Sheet
Core Concepts Matrix
| Feature | Scalar Atomics (AtomicInteger) | Scalable Adders (LongAdder) | Intrinsic Lock (synchronized) |
| :— | :— | :— | :— |
| Concurrency Model | Lock-free (CAS loop) | Lock-free sharded cells | Blocking monitor lock |
| Contention Profile | Low-to-medium contention | High write contention | General mutual exclusion |
| Cache Line Behavior| Cache Bouncing on write hotspots| Isolated per cell via @Contended | Monitor state transitions |
| Memory Footprint | 16–24 bytes per instance | Dynamically scales array | 16-byte object header |
| Multi-field Atomicity| Single field only | Single primitive counter | Arbitrary statement blocks |
Key Takeaways
- Non-blocking primitives: Atomic classes achieve lock-free thread safety using CPU hardware CAS (
LOCK CMPXCHGon x86, LL/SC on ARM) andvolatilevisibility. - Check-Then-Act anti-pattern: Never combine separate atomic calls like
if (atom.get() == 0) atom.set(1);—always usecompareAndSet(). - Scaling writes: When write contention on an
AtomicLongcauses CPU thrashing, replace it withLongAdder. - Heap reduction: For millions of entity instances, replace per-object
AtomicIntegerinstances withAtomicIntegerFieldUpdaterover a primitivevolatile int.
Red Flags & Anti-Patterns to Avoid
- ❌ Assuming Atomics are always faster than
synchronized: Under massive write contention, scalar atomics spin in CAS loops consuming 100% CPU, whereas thread parking in locks can yield higher overall system efficiency. - ❌ Using
AtomicReferenceto mutate mutable objects:AtomicReferenceonly ensures thread-safe swapping of the reference pointer; mutating internal fields of a referenced object is not thread-safe. - ❌ Omitting
volatileon fields used byAtomicIntegerFieldUpdater: The target field must be declaredvolatileand cannot beprivate, or the updater factory will throw anIllegalArgumentException.