Question 10 · Section 9

How do AtomicInteger, AtomicLong work?

AtomicInteger and AtomicLong are thread-safe wrappers over the int and long primitives.

Language versions: English Russian Ukrainian

Junior Level

Basic Understanding

AtomicInteger and AtomicLong are thread-safe wrappers over the int and long primitives.

Why: when multiple threads simultaneously modify a regular int variable, updates are lost. AtomicInteger solves this by guaranteeing atomicity of each operation without locks. They provide atomic (indivisible) operations without locks via CAS. Atomic = other threads cannot see an intermediate state.

Creation and Basic Operations

// Creation
AtomicInteger atomicInt = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0L);

// Increment/decrement
atomicInt.incrementAndGet();  // ++i (returns new)
atomicInt.getAndIncrement();  // i++ (returns old)
atomicInt.decrementAndGet();  // --i
atomicInt.getAndDecrement();  // i--

// Arithmetic
atomicInt.addAndGet(10);      // i += 10
atomicInt.getAndAdd(10);      // returns old, then += 10

// Setting
atomicInt.set(100);           // Direct set
atomicInt.getAndSet(200);     // Returns old, sets new

// CAS
atomicInt.compareAndSet(200, 300); // If == 200, set to 300

// Getting
int value = atomicInt.get();  // Current value

Example: Request Counter

public class RequestCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void handleRequest(Request req) {
        int currentCount = count.incrementAndGet();
        System.out.println("Request #" + currentCount);
        // Processing...
    }

    public int getTotalRequests() {
        return count.get();
    }
}

Why not just int?

// NOT safe:
int count = 0;
count++; // 3 operations: read → modify → write
// Two threads may read the same value and lose an update

// Safe:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Atomic operation — CAS at CPU level

Main Methods

Method Description Returns
get() Current value int/long
set(v) Set value void
incrementAndGet() ++i New value
getAndIncrement() i++ Old value
addAndGet(delta) i += delta New value
getAndAdd(delta) i += delta Old value
compareAndSet(exp, upd) CAS true/false
updateAndGet(fn) Update via function New value

Middle Level

Internal Structure

public class AtomicInteger extends Number implements java.io.Serializable {
    // 1. volatile field for visibility
    private volatile int value;

    // 2. VarHandle for direct memory access (Java 9+)
    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 Error(e);
        }
    }

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
}

Different Write Methods

AtomicInteger counter = new AtomicInteger(0);

// 1. set(v) — regular volatile write
counter.set(100);
// Immediately visible to all threads, full memory barrier

// 2. lazySet(v) — relaxed write (Java 9+: setOpaque)
counter.lazySet(100);
// Does not guarantee immediate visibility, but faster because it doesn't set a full memory barrier — the processor is not required to immediately synchronize cache lines with other cores.
// Useful for resetting state that will soon be read by the same thread

// 3. compareAndSet(exp, upd) — full CAS
counter.compareAndSet(0, 100);
// If == 0, set to 100. Full barrier.

// 4. updateAndGet(fn) — update via function
counter.updateAndGet(x -> x * 2); // Multiply by 2
Method Happens-Before Speed When to use
set(v) Yes Standard Regular write
lazySet(v) No (eventual) Faster Flag reset
compareAndSet Yes Standard Lock-free algorithms
weakCompareAndSet No Faster Optimized cycles

Safety on 32-bit Systems

// On a 32-bit JVM:
long value = 0x0123456789ABCDEFL;
// Written in TWO steps:
// 1. 0x89ABCDEF (lower 32 bits)
// 2. 0x01234567 (upper 32 bits)
// Another thread may see a "mixed" value!

// AtomicLong guarantees atomicity:
AtomicLong atomicLong = new AtomicLong(0x0123456789ABCDEFL);
// Uses CMPXCHG8B instruction or internal locking

updateAndGet — Flexible Atomic Operation

AtomicInteger flags = new AtomicInteger(0);

// Atomically set a bit
flags.updateAndGet(current -> current | 0x01);

// Atomically clear a bit
flags.updateAndGet(current -> current & ~0x01);

// Atomically toggle a bit
flags.updateAndGet(current -> current ^ 0x01);

False Sharing (cache line problem)

When two AtomicInteger instances are next to each other in an array, they may end up in the same 64-byte CPU cache line. If thread A writes to counters[0] and thread B reads counters[1] — they compete for one cache line, even though these are different counters.

Solution: @Contended annotation (Java 8+, requires -XX:-RestrictContended) — adds padding around the field so it occupies a separate cache line.

@sun.misc.Contended
AtomicInteger counter; // 64 bytes of padding before and after

For long[] arrays, false sharing is real:

long[] counters = new long[8];
// counters[0] and counters[1] in the same cache line → two threads compete

When AtomicInteger is a poor choice

  1. High contention (100+ threads): CAS contention kills performance — use LongAdder
  2. Compound operations: atomically updating two fields — need synchronized
  3. Long computations between read and write: CAS will constantly fail — better to use Lock

Senior Level

Under the Hood: VarHandle and Memory Access API

Java 9+ uses VarHandle instead of Unsafe:

// Atomic via VarHandle
public final int getAndAdd(int delta) {
    return (int)VALUE.getAndAdd(this, delta);
}

// VarHandle translates to native instructions:
// VALUE.getAndAdd → lock xadd [memory], delta (x86)

getAndAdd at x86 Level

; lock xadd — atomic addition returning old value
; eax = delta, [memory] = counter

lock xadd [memory], eax
; [memory] = [memory] + eax
; eax = old value of [memory]

AtomicIntegerFieldUpdater — Memory Savings

For cases where millions of objects need an atomic int:

// BAD: millions of extra AtomicInteger objects
class User {
    AtomicInteger loginAttempts = new AtomicInteger(0); // 24 bytes × 1M = 24MB
}

// GOOD: one field + one static updater
class User {
    volatile int loginAttempts = 0; // 4 bytes × 1M = 4MB

    private static final AtomicIntegerFieldUpdater<User> UPDATER =
        AtomicIntegerFieldUpdater.newUpdater(User.class, "loginAttempts");

    public int incrementAttempts() {
        return UPDATER.incrementAndGet(this);
    }
}

CAS Contention and ID Generator

// BAD: global ID generator at 1000 RPS
AtomicLong idGenerator = new AtomicLong(0);
public long nextId() {
    return idGenerator.incrementAndGet(); // High contention!
}

// GOOD: ID range allocation
public class IdRangeGenerator {
    private final AtomicLong nextRangeStart = new AtomicLong(0);
    private static final long RANGE_SIZE = 1000;

    // Issues a batch of IDs — each service/thread gets its own range
    public IdRange allocateRange() {
        long start = nextRangeStart.getAndAdd(RANGE_SIZE);
        return new IdRange(start, start + RANGE_SIZE);
    }
}

Performance and Highload

Benchmark (approximate)

Operation Time Note
get() ~1 ns Volatile read
set() ~5 ns Volatile write
incrementAndGet() (no contention) ~5-10 ns One CAS
incrementAndGet() (100 threads) ~100+ ns CAS contention
lazySet() ~2-3 ns Without full barrier

Optimization via Batching

// Local counter + atomic flush
public class BatchedCounter {
    private int local = 0;
    private final AtomicInteger flushed = new AtomicInteger(0);

    public synchronized void increment() {
        local++;
        if (local >= 100) { // Flush every 100
            flushed.addAndGet(local);
            local = 0;
        }
    }

    public int getTotal() {
        synchronized(this) {
            return flushed.get() + local;
        }
    }
}

Diagnostics

Bytecode Analysis

javap -c -p AtomicInteger.class
public final int getAndAddInt(int);
  Code:
    0: aload_0
    1: invokevirtual get
    4: iload_1
    5: iadd
    6: dup
    7: invokevirtual compareAndSet  // ← CAS call

Profiling CAS Failures

// Track CAS attempts vs successes
AtomicLong successes = new AtomicLong(0);
AtomicLong attempts = new AtomicLong(0);

for (int i = 0; i < 1_000_000; i++) {
    attempts.incrementAndGet();
    if (counter.compareAndSet(expected, next)) {
        successes.incrementAndGet();
    }
}

double successRate = (double) successes.get() / attempts.get();
System.out.println("CAS success rate: " + (successRate * 100) + "%");
// < 50% → high contention

Best Practices

  1. AtomicInteger for counters — under moderate contention
  2. AtomicLong for 64-bit counters — safe on 32-bit JVMs
  3. LongAdder under high contention — distributed cells
  4. lazySet for flag resets — faster than set
  5. updateAndGet for complex updates — atomic function
  6. AtomicIntegerFieldUpdater for memory savings — millions of objects
  7. ID range allocation — instead of global AtomicLong at 1000+ RPS

Interview Cheat Sheet

Must know:

  • AtomicInteger/AtomicLong — thread-safe wrappers over int/long, provide atomicity without locks
  • Work via CAS (compare-and-swap) — a hardware CPU instruction (lock cmpxchg on x86)
  • Java 9+ use VarHandle instead of Unsafe for memory access
  • incrementAndGet() = spin cycle: read → compute → CAS → retry on failure
  • lazySet() — relaxed write without full memory barrier, faster for flag resets
  • Under high contention (100+ threads), CAS contention kills performance — need LongAdder
  • AtomicLong guarantees atomic read/write even on 32-bit JVMs (where long is written in 2 steps)
  • AtomicIntegerFieldUpdater saves memory when millions of objects need an atomic field

Frequent follow-up questions:

  • How does getAndIncrement() differ from incrementAndGet()? — The first returns the old value (i++), the second — the new (++i)
  • Why is CAS better than synchronized? — No context switch, deadlock impossible, ~5ns vs ~1000ns under low contention
  • What is false sharing? — Two AtomicInteger in the same CPU cache line compete even at different addresses; solved with @Contended
  • When is AtomicInteger a poor choice? — High contention (LongAdder is better), compound operations (need synchronized), long computations between read/write

Red flags (do NOT say):

  • “Atomic uses synchronized internally” — no, it’s lock-free CAS
  • “AtomicInteger is always faster than synchronized” — no, at 8+ threads synchronized may be faster
  • “compareAndSet blocks the thread on failure” — no, it just returns false, the thread spins in a loop

Related topics:

  • [[11. What is the advantage of Atomic classes over synchronized]]
  • [[12. What is Thread Pool]]
  • [[15. What does ExecutorService do]]
  • [[18. What is deadlock]]