🧵 Section 9 · Question #10

How do AtomicInteger, AtomicLong work

Their internal execution relies on two core architectural pillars:


🟢 Junior Level

30-Second Summary

AtomicInteger and AtomicLong are thread-safe, lock-free numeric wrappers around primitive int and long types located in the java.util.concurrent.atomic package. They enable atomic (indivisible) read-modify-write operations without thread blocking, operating system monitor acquisition, or kernel context switching.

Their internal execution relies on two core architectural pillars:

  1. volatile storage: The underlying primitive field is declared as volatile, guaranteeing immediate memory visibility and preventing compiler/CPU reordering via the Java Memory Model (JMM) happens-before guarantee.
  2. Hardware CAS (Compare-And-Swap): State updates execute via optimistic hardware instructions that verify whether the memory cell still holds the expected value before applying mutations, seamlessly retrying if concurrent collisions occur.

The Primitive Non-Atomicity Problem

A seemingly simple operation such as count++ or count += 5 is not atomic in Java bytecode. It decomposes into three distinct machine operations:

  1. Read (getfield / iload): Load the current value from main memory or CPU cache into a register.
  2. Modify (iadd): Increment the value inside the CPU arithmetic logic unit (ALU).
  3. Write (putfield / istore): Store the updated value from the register back into memory.

When two threads execute count++ concurrently, both threads can read the same starting value before either completes the write step, resulting in a lost update (Race Condition).


Core Operations in Java 21

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class AtomicUsageDemo {
    private final AtomicInteger counter = new AtomicInteger(0);
    private final AtomicLong globalIdSeq = new AtomicLong(1000L);

    public void demonstrateOperations() {
        // 1. Increment and Decrement
        int next = counter.incrementAndGet(); // Prefix increment: ++i (returns 1)
        int prev = counter.getAndIncrement(); // Postfix increment: i++ (returns 1, becomes 2)
        counter.decrementAndGet();            // Prefix decrement: --i (returns 1)

        // 2. Arithmetic Addition
        counter.addAndGet(10);                // i += 10 (returns 11)
        int oldVal = counter.getAndAdd(5);    // Returns 11, state becomes 16

        // 3. Conditional Atomic Swap (CAS)
        boolean swapped = counter.compareAndSet(16, 100); // true if state was 16

        // 4. Functional Atomic Update (Java 8+)
        // Runs a side-effect-free lambda inside an internal optimistic CAS loop
        int doubled = counter.updateAndGet(x -> x * 2);

        // 5. Binary Accumulation
        int maxVal = counter.accumulateAndGet(250, Math::max);
    }
}

🟡 Middle Level

Internal Architecture: volatile + VarHandle (Java 9+)

Historically, AtomicInteger relied on the internal sun.misc.Unsafe class. In Java 9+, OpenJDK transitioned to java.lang.invoke.VarHandle, providing type-safe low-level memory access while maintaining direct hardware compilation paths.

public class AtomicInteger extends Number implements java.io.Serializable {
    // 1. Underlying primitive storage: strictly volatile
    private volatile int value;

    // 2. Variable handle descriptor for atomic memory operations
    private static final VarHandle VALUE;

    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public final int incrementAndGet() {
        // Translates directly to hardware 'lock xadd' on x86
        return (int) VALUE.getAndAdd(this, 1) + 1;
    }
}

Non-Atomicity of 64-Bit long on 32-Bit JVMs (Word Tearing)

The Java Language Specification (JLS §17.7 Non-Atomic Treatment of double and long) states:

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half.

On 32-bit hardware architectures, writing a 64-bit primitive requires two distinct CPU instructions (one for the low 32 bits, one for the high 32 bits). Concurrent reads during these halves cause Word Tearing:

Thread 1 writes: 0x00000001_00000000L
Thread 2 writes: 0x00000000_FFFFFFFFL

Thread 3 reads low half from Thread 1 and high half from Thread 2:
Observed Value: 0x00000001_FFFFFFFFL (Corrupted garbage state!)

AtomicLong completely prevents Word Tearing: Even on 32-bit architectures, its internal volatile modifier combined with 64-bit atomic machine instructions (such as CMPXCHG8B on x86-32) guarantees that 64-bit reads and writes are physically indivisible.


Write Ordering Semantics: set() vs. lazySet()

AtomicInteger and AtomicLong offer distinct write modes with different CPU bus synchronization costs:

Method JMM Memory Semantic Hardware Memory Barriers Cross-Core Visibility Latency
set(v) Volatile store StoreStore + StoreLoad Immediate (flushes CPU store buffer) ~5–10 ns
lazySet(v) Ordered / Release store StoreStore only Eventual (omits StoreLoad pipeline stall) ~1–2 ns
compareAndSet(e, u) Full Bidirectional Fence Full Fence (StoreLoad) Immediate upon atomic success ~10–15 ns
AtomicInteger flag = new AtomicInteger(0);

// lazySet is ideal for de-referencing nodes or resetting flags in ring buffers:
// Guarantees previous writes are visible, but avoids the heavy StoreLoad fence
flag.lazySet(1);

🔴 Senior Level

Hardware Translation: LOCK XADD vs. CAS Retry Loops

The HotSpot C2 JIT compiler applies specialized instruction optimizations to AtomicInteger operations:

1. Hardware-Direct: incrementAndGet() & addAndGet()

On x86 architectures, the C2 compiler optimizes these methods into a single atomic CPU instruction:

; rcx = address of 'value' field
; eax = delta (1)
lock xadd [rcx], eax
; Atomically exchanges and adds in a single CPU cycle; returns old value in eax

LOCK XADD executes at the hardware memory controller level without requiring a software retry loop. It is significantly faster than a generic CAS loop when thread contention is moderate.

2. Software Loop: updateAndGet()

Because updateAndGet allows arbitrary user logic via IntUnaryOperator, it cannot map to a single hardware instruction. It executes an optimistic spin-retry loop:

public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev = get(), next = 0;
    for (;;) {
        next = updateFunction.applyAsInt(prev);
        if (compareAndSet(prev, next)) {
            return next;
        }
        prev = get(); // Re-read current value upon CAS collision
    }
}

[!CAUTION] Functions passed to updateAndGet() must be pure and side-effect free!
Under concurrent contention, the lambda may be evaluated multiple times. Triggering network requests, queue publications, or external mutations inside this lambda will cause duplicate executions.


High Contention: Degradation and Migration to LongAdder

Under extreme write contention across dozens or hundreds of threads, scalar atomic counters collapse:

  • Competing threads repeatedly invalidate the target 64-byte L1/L2 cache line via LOCK CMPXCHG / LOCK XADD.
  • Cache Bouncing floods the interconnect bus (Intel UPI / AMD Infinity Fabric) with cache coherency traffic.
  • Throughput plummets while CPU usage spikes to 100%.
// ❌ Scalability bottleneck under 50+ concurrent writing threads:
AtomicLong bottleneck = new AtomicLong();

// ✅ Scalable solution under high write contention:
java.util.concurrent.atomic.LongAdder scalableCounter = new java.util.concurrent.atomic.LongAdder();
scalableCounter.increment(); // Stripes writes across independent, padded @Contended cells
long total = scalableCounter.sum(); // Aggregate when reading

High-Load Design: ID Range Allocation & Memory Density

1. ID Range Allocation (Batching)

Directly invoking AtomicLong.incrementAndGet() for high-RPS sequence generation (e.g., 200,000 requests/sec) exhausts CPU cache bandwidth. Batching allocations reduces memory bus transactions by orders of magnitude:

public class BatchIdGenerator {
    private final AtomicLong globalSequence = new AtomicLong(1);
    private static final long BATCH_SIZE = 10_000;

    private static final ThreadLocal<LocalRange> localRange = 
            ThreadLocal.withInitial(() -> new LocalRange(0, 0));

    private record LocalRange(long current, long max) {}

    public long nextId() {
        LocalRange range = localRange.get();
        if (range.current < range.max) {
            localRange.set(new LocalRange(range.current + 1, range.max));
            return range.current;
        }

        // Amortize CAS contention: hit shared global state only once every 10,000 IDs!
        long newStart = globalSequence.getAndAdd(BATCH_SIZE);
        localRange.set(new LocalRange(newStart + 1, newStart + BATCH_SIZE));
        return newStart;
    }
}

2. Heap Footprint Reduction: AtomicIntegerFieldUpdater

When managing millions of object instances (e.g., active user sessions in an in-memory cache), wrapping each counter in an AtomicInteger incurs massive memory overhead:

  • 10,000,000 AtomicInteger instances: ~240 MB heap (object header + reference + alignment).
  • 10,000,000 primitive volatile int fields managed via a single static AtomicIntegerFieldUpdater: ~40 MB heap.

4 Tricky Questions

1. Why can reading a regular long on a 32-bit JVM return a corrupted, torn value (“Word Tearing”), whereas AtomicLong guarantees data integrity?

Answer: Under JLS §17.7, writes to standard (non-volatile) 64-bit long and double primitives on 32-bit JVMs are treated as two separate 32-bit write instructions (high bits and low bits). If Thread A updates the lower 32 bits and Thread B concurrently updates both halves, an unsynchronized reader thread can observe the lower half written by Thread A and the upper half written by Thread B. This produces a fabricated 64-bit number that was never written by any thread (“Word Tearing”).
AtomicLong prevents this because its internal field is declared as volatile. The JVM is required by the Java Memory Model to treat all volatile reads and writes as atomic operations, utilizing atomic 64-bit CPU instructions (such as CMPXCHG8B on x86 or SSE register load/stores) even on 32-bit runtime platforms.


2. What is the microarchitectural difference in generated machine code between incrementAndGet() and updateAndGet(x -> x + 1) on x86?

Answer:

  • incrementAndGet() is recognized as an intrinsic by the HotSpot C2 compiler and emits a single atomic x86 hardware instruction: lock xadd. The CPU performs the addition and returns the old value directly in silicon in a bounded number of cycles, completely eliminating software retry loops.
  • updateAndGet(x -> x + 1) takes an arbitrary IntUnaryOperator callback. C2 cannot map an arbitrary function to a dedicated hardware arithmetic instruction. Instead, it generates a software CAS retry loop (for(;;)) that repeatedly executes the lambda, reads the volatile state, and executes lock cmpxchg. Under concurrent write contention, updateAndGet will loop and repeatedly invoke the lambda, incurring much higher CPU and cache coherency overhead.

3. Why must the lambda passed to updateAndGet() or accumulateAndGet() be strictly free of side effects?

Answer: Inside updateAndGet() and accumulateAndGet(), the user-supplied function is executed inside an optimistic CAS retry loop. If another thread modifies the value before the current thread’s CAS completes, the CAS fails and the loop restarts.
During each failed iteration, the lambda is re-executed with the newly observed value. If the lambda contains side effects (such as sending an HTTP request, publishing to a Kafka topic, mutating an external list, or incrementing another counter), these side effects will execute multiple times unpredictably, causing severe semantic bugs and duplicate operations.


4. How does lazySet() work, what memory barriers does it emit, and when is it preferred over set()?

Answer: lazySet() implements an ordered / release store operation. In terms of memory barriers, it inserts a StoreStore barrier prior to the write, ensuring that all prior program writes are made visible before this write occurs. However, it omits the expensive StoreLoad memory barrier immediately following the write.
Without StoreLoad, the CPU is not forced to drain its hardware Store Buffer into the L1 cache before continuing execution. As a result, other CPU cores may not observe the written value immediately (it offers eventual consistency, typically within tens of nanoseconds), but the writing core executes the instruction with near-zero latency (~1–2 ns vs ~5–10 ns for a volatile set()). It is preferred in lock-free ring buffers (like LMAX Disruptor) or queues when resetting node references or publishing sequence numbers where a subsequent volatile write or release operation will flush the buffer anyway.


🎯 Interview Cheat Sheet

Core Concepts Matrix

| Dimension | AtomicInteger / AtomicLong | LongAdder | synchronized / ReentrantLock | | :— | :— | :— | :— | | Concurrency Paradigm| Lock-Free (Hardware CAS) | Striped Lock-Free Cells | Blocking Mutex | | x86 Machine Instruction | LOCK XADD / LOCK CMPXCHG | Distributed LOCK XADD | Monitor Enter/Exit (futex) | | Contention Performance | High at low/medium; degrades at 30+ threads | Superior at ultra-high contention | Moderate (parks threads cleanly) | | Memory Footprint | Low (object overhead per counter) | Higher (cell array + padding) | Minimal per primitive; monitor cost | | Functional Updates | Yes (updateAndGet, accumulateAndGet) | Additive only (add, increment) | Full arbitrary critical sections |


Key Takeaways

  • Hardware-Direct Atomicity: incrementAndGet() compiles to native lock xadd on x86, avoiding software retry loops.
  • Word-Tearing Defense: AtomicLong guarantees atomic 64-bit updates even on 32-bit JVMs.
  • Pure Functions Only: Never include side effects inside updateAndGet() or accumulateAndGet() because CAS collisions trigger repeated execution.
  • High-Throughput Scaling: Under massive write contention across many cores, substitute AtomicLong with LongAdder.

Red Flags & Anti-Patterns to Avoid

  • ❌ Chaining atomic calls unsafely: Writing if (atomic.get() == 0) { atomic.set(1); } is a race condition; use atomic.compareAndSet(0, 1) instead.
  • ❌ Putting I/O in functional atomic updaters: Performing network or database calls inside updateAndGet() causes duplicated I/O on CAS retry.
  • ❌ Using AtomicLong as a high-RPS sequence generator: Causes interconnect bus thrashing; batch increments via ID Range Allocation or use striping.