🚀AdvancedAdvanced Concurrency

Atomic Variables

Use AtomicInteger, AtomicLong, and AtomicReference for lock-free thread-safe operations.

Overview

Atomic variables in java.util.concurrent.atomic provide lock-free, thread-safe operations on single values. They use hardware-level compare-and-swap (CAS) instructions instead of locks, making them faster than synchronized or ReentrantLock for simple counter and flag patterns. The key operation is compareAndSet(expected, update): if the current value equals expected, update it atomically and return true; otherwise leave it unchanged and return false. All other atomic methods (incrementAndGet, getAndAdd, etc.) are built on CAS internally. Atomic classes work best for independent, single-value updates. For compound operations involving multiple variables, you still need locks.

Key Points

  • •AtomicInteger, AtomicLong, AtomicBoolean for primitive wrappers
  • •AtomicReference<T> for object references
  • •AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray for arrays
  • •incrementAndGet() is equivalent to ++i (returns new value)
  • •getAndIncrement() is equivalent to i++ (returns old value)
  • •compareAndSet(expected, update) is the core CAS operation
  • •getAndUpdate(fn) and updateAndGet(fn) apply a function atomically
  • •accumulateAndGet(x, fn) accumulates using a binary operator
  • •LongAdder is faster than AtomicLong for high-contention counters
  • •Atomic variables do not eliminate race conditions in compound operations across multiple variables

Examples

AtomicInteger as a thread-safe counter

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) counter.incrementAndGet();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) counter.incrementAndGet();
        });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println(counter.get()); // always 200000
    }
}

With a plain int and no synchronization, the result would be less than 200000 due to lost updates. AtomicInteger guarantees all increments are reflected.

compareAndSet for optimistic locking patterns

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) {
        AtomicInteger value = new AtomicInteger(10);

        // Only update if current value is still 10
        boolean updated = value.compareAndSet(10, 20);
        System.out.println("Updated: " + updated + ", value: " + value.get()); // true, 20

        // Now value is 20, so CAS with expected=10 fails
        updated = value.compareAndSet(10, 30);
        System.out.println("Updated: " + updated + ", value: " + value.get()); // false, 20

        // Custom accumulation: multiply by 2 atomically
        int result = value.updateAndGet(v -> v * 2);
        System.out.println("After doubling: " + result); // 40
    }
}

compareAndSet is the building block for all atomic operations. updateAndGet retries internally if a concurrent write happens between the read and the CAS, so the function may be called more than once.

AtomicReference for lock-free object swapping

import java.util.concurrent.atomic.AtomicReference;

public class Main {
    record Config(String host, int port) {}

    static AtomicReference<Config> config =
        new AtomicReference<>(new Config("localhost", 8080));

    public static void main(String[] args) {
        // Hot-swap config without locking
        Config old = config.get();
        Config updated = new Config(old.host(), 9090);
        boolean swapped = config.compareAndSet(old, updated);
        System.out.println("Swapped: " + swapped);
        System.out.println("New config: " + config.get());
    }
}

AtomicReference is perfect for immutable config objects that are periodically replaced. Because Config is a record (immutable), reading threads always see a consistent snapshot without needing locks.

Common Mistakes

  • Using AtomicInteger but still doing non-atomic compound reads. For example, reading get() and then conditionally calling set() is not atomic; another thread can modify the value between the get() and set(). Use compareAndSet or updateAndGet instead.
  • Assuming AtomicReference prevents all races. It only makes the reference swap atomic. If the referenced object is mutable, readers can still observe partial state. Prefer immutable value objects with AtomicReference.
  • Using AtomicLong in a highly contended hot path. LongAdder performs better under contention because it shards the counter across multiple cells and sums them on read.
  • Calling get() twice expecting the same value. Between two get() calls, another thread can change the value. Store the result once: int v = atomic.get(); then work with v.

Note

LongAdder and LongAccumulator (Java 8+) outperform AtomicLong when many threads increment a counter simultaneously. The tradeoff is that sum() does not return a consistent snapshot if updates are ongoing, but for aggregate counters (like hit counts) that is acceptable.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What does AtomicInteger.compareAndSet(10, 20) do?