Skip to main content

Command Palette

Search for a command to run...

Lock-Free Programming - Java (Part 4)

Atomic Variables in Java — Your Lock-Free Toolbox

Published
9 min read

Now that we understand CAS, it's time to meet the Java classes that wrap it in a developer-friendly API. The java.util.concurrent.atomic package is your lock-free toolbox — a collection of thread-safe, non-blocking classes that cover most concurrency needs without ever grabbing a lock.


The Atomic Family

Let's explore each group with practical examples.


AtomicInteger and AtomicLong

These are the workhorses. They wrap an int or long with CAS-based operations.

Key Operations

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    private final AtomicInteger counter = new AtomicInteger(0);

    public void demonstrate() {
        // Basic read/write (volatile semantics)
        int value = counter.get();              // Read
        counter.set(42);                         // Write
        
        // Atomic increments
        int oldVal = counter.getAndIncrement();  // returns old, then +1
        int newVal = counter.incrementAndGet();  // +1, then returns new
        
        // Atomic addition
        counter.getAndAdd(10);                   // returns old, adds 10
        counter.addAndGet(10);                   // adds 10, returns new
        
        // Compare-And-Set
        boolean success = counter.compareAndSet(52, 100);
        
        // Functional update (Java 8+)
        counter.updateAndGet(current -> current * 2);
        counter.getAndUpdate(current -> Math.min(current, 1000));
        
        // Accumulate (Java 8+)
        counter.accumulateAndGet(5, Integer::max);
    }
}

Practical Example: Thread-Safe ID Generator

import java.util.concurrent.atomic.AtomicLong;

public class IdGenerator {
    private final AtomicLong nextId = new AtomicLong(1);
    
    public long generateId() {
        return nextId.getAndIncrement();
    }
}

This is simpler, faster, and more correct than:

// ❌ Lock-based alternative — heavier
public class LockedIdGenerator {
    private long nextId = 1;
    
    public synchronized long generateId() {
        return nextId++;
    }
}

AtomicBoolean

AtomicBoolean is useful for one-time initialization flags, shutdown signals, and try-once patterns.

Practical Example: Run-Once Initializer

import java.util.concurrent.atomic.AtomicBoolean;

public class OneTimeInitializer {
    private final AtomicBoolean initialized = new AtomicBoolean(false);
    
    public void initialize() {
        // Only ONE thread will enter this block, ever
        if (initialized.compareAndSet(false, true)) {
            System.out.println("Initializing... (happens exactly once)");
            // ... expensive setup ...
        }
    }
}

AtomicReference

This is where lock-free programming gets really powerful. AtomicReference lets you atomically swap entire object references.

Practical Example: Lock-Free Configuration

import java.util.concurrent.atomic.AtomicReference;
import java.util.Collections;
import java.util.Map;

public class LockFreeConfig {
    // Immutable config record (Java 16+, use a class for earlier versions)
    record Config(String dbHost, int dbPort, int maxThreads, 
                  Map<String, String> properties) {
        // Defensive copy ensures immutability
        Config {
            properties = Map.copyOf(properties);
        }
    }
    
    private final AtomicReference<Config> config;
    
    public LockFreeConfig(Config initial) {
        this.config = new AtomicReference<>(initial);
    }
    
    /**
     * Atomically reads the current config.
     */
    public Config getConfig() {
        return config.get();
    }
    
    /**
     * Atomically updates a single property.
     * Uses CAS loop: creates a new immutable Config on each attempt.
     */
    public void setProperty(String key, String value) {
        Config current, next;
        do {
            current = config.get();
            // Build new immutable config with the property added
            var newProps = new java.util.HashMap<>(current.properties());
            newProps.put(key, value);
            next = new Config(
                current.dbHost(), current.dbPort(), 
                current.maxThreads(), newProps
            );
        } while (!config.compareAndSet(current, next));
    }
    
    /**
     * Atomically replaces the entire config, but only if 
     * nobody else has changed it since we last read it.
     */
    public boolean replaceConfig(Config expected, Config replacement) {
        return config.compareAndSet(expected, replacement);
    }
}

The Pattern: Immutable Objects + CAS

Golden Rule: Always use immutable objects with AtomicReference. If you CAS a mutable reference and then modify the object, other threads might see a half-modified object. Immutability eliminates this entire class of bugs.


AtomicStampedReference and AtomicMarkableReference

These solve the ABA problem. Here's a quick preview:

AtomicStampedReference

Pairs a reference with an integer stamp (version number). Both must match for a CAS to succeed.

import java.util.concurrent.atomic.AtomicStampedReference;

public class StampedExample {
    // Initial value "A" with stamp 0
    private final AtomicStampedReference<String> ref = 
        new AtomicStampedReference<>("A", 0);
    
    public void update() {
        int[] stampHolder = new int[1];
        String current = ref.get(stampHolder);  // Gets value AND stamp
        int currentStamp = stampHolder[0];
        
        // CAS checks BOTH the reference AND the stamp
        ref.compareAndSet(
            current, "B",           // expected, new reference
            currentStamp, currentStamp + 1  // expected, new stamp
        );
    }
}

AtomicMarkableReference

Like AtomicStampedReference, but with a boolean mark instead of an int stamp. Useful for marking nodes as "logically deleted" in lock-free data structures.

import java.util.concurrent.atomic.AtomicMarkableReference;

public class MarkableExample {
    private final AtomicMarkableReference<String> ref = 
        new AtomicMarkableReference<>("Hello", false);
    
    public void markAsDeleted() {
        String current;
        do {
            current = ref.getReference();
        } while (!ref.compareAndSet(current, current, false, true));
        // Same object, but now marked as "deleted"
    }
}

Atomic Arrays

When you have multiple values that each need atomic access, use atomic arrays:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicArrayDemo {
    // Thread-safe array of counters
    private final AtomicIntegerArray counters = new AtomicIntegerArray(10);
    
    public void incrementBucket(int index) {
        counters.incrementAndGet(index);
    }
    
    public int getBucket(int index) {
        return counters.get(index);
    }
    
    public int getTotal() {
        int total = 0;
        for (int i = 0; i < counters.length(); i++) {
            total += counters.get(i);
        }
        return total;
    }
}

Why not just AtomicInteger[]? An AtomicIntegerArray stores the values in a contiguous block, which is better for cache performance. An AtomicInteger[] stores each counter as a separate heap object, scattered in memory.


LongAdder and LongAccumulator (Java 8+)

Remember the cache line bouncing problem from Article 3? When dozens of threads CAS the same AtomicLong, performance degrades because only one core can own that cache line at a time.

LongAdder solves this by spreading the value across multiple cells:

Each thread adds to its own cell (determined by a hash of the thread). When you call sum(), LongAdder adds up all the cells.

import java.util.concurrent.atomic.LongAdder;

public class HighThroughputCounter {
    private final LongAdder requestCount = new LongAdder();
    
    public void recordRequest() {
        requestCount.increment();  // Almost zero contention!
    }
    
    public long getRequestCount() {
        return requestCount.sum();  // Returns approximate current count
    }
}

When to Use LongAdder vs AtomicLong

LongAccumulator: The Generalized Version

LongAccumulator lets you specify any associative function, not just addition:

import java.util.concurrent.atomic.LongAccumulator;

public class AccumulatorExamples {
    // Maximum value seen across all threads
    private final LongAccumulator max = 
        new LongAccumulator(Long::max, Long.MIN_VALUE);
    
    // Minimum value seen across all threads
    private final LongAccumulator min = 
        new LongAccumulator(Long::min, Long.MAX_VALUE);
    
    // Sum (equivalent to LongAdder)
    private final LongAccumulator sum = 
        new LongAccumulator(Long::sum, 0);
    
    public void recordLatency(long latencyMs) {
        max.accumulate(latencyMs);
        min.accumulate(latencyMs);
        sum.accumulate(latencyMs);
    }
    
    public void printStats() {
        System.out.printf("Max: %d ms, Min: %d ms, Total: %d ms%n",
            max.get(), min.get(), sum.get());
    }
}

VarHandle: The Modern API (Java 9+)

VarHandle is the successor to sun.misc.Unsafe. It provides type-safe, granular control over memory access modes:

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandleDemo {
    private volatile int counter = 0;
    private volatile long timestamp = 0;
    
    private static final VarHandle COUNTER;
    private static final VarHandle TIMESTAMP;
    
    static {
        try {
            var lookup = MethodHandles.lookup();
            COUNTER = lookup.findVarHandle(VarHandleDemo.class, "counter", int.class);
            TIMESTAMP = lookup.findVarHandle(VarHandleDemo.class, "timestamp", long.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    
    public void increment() {
        // CAS loop, just like AtomicInteger
        int current;
        do {
            current = (int) COUNTER.get(this);
        } while (!COUNTER.compareAndSet(this, current, current + 1));
    }
    
    public void weakIncrement() {
        // Weak CAS — may spuriously fail but can be faster on ARM
        int current;
        do {
            current = (int) COUNTER.get(this);
        } while (!COUNTER.weakCompareAndSet(this, current, current + 1));
    }
    
    public void relaxedWrite(long ts) {
        // Opaque write — no ordering guarantees, but atomicity guaranteed
        TIMESTAMP.setOpaque(this, ts);
    }
}

VarHandle Access Modes

VarHandle offers multiple access modes with different strength guarantees:

Mode Ordering Atomicity Use Case
get/set Volatile (full fence) Yes Default safe choice
getOpaque/setOpaque None Yes Counters where ordering doesn't matter
getAcquire/setRelease Acquire/Release Yes Paired write→read visibility
compareAndSet Volatile Yes Standard CAS
weakCompareAndSet Volatile Yes (may spuriously fail) CAS loops (can be faster on ARM)

Recommendation: Stick with AtomicInteger/AtomicReference for most use cases. Reach for VarHandle when you need relaxed access modes for maximum performance, or when you need lock-free operations on fields of existing objects without wrapping them in Atomic classes.


Choosing the Right Atomic Class


Putting It All Together: A Lock-Free Statistics Tracker

Here's a real-world example combining multiple atomic classes:

import java.util.concurrent.atomic.*;

public class LockFreeStats {
    private final LongAdder requestCount = new LongAdder();
    private final LongAdder totalLatency = new LongAdder();
    private final LongAccumulator maxLatency = 
        new LongAccumulator(Long::max, 0);
    private final LongAccumulator minLatency = 
        new LongAccumulator(Long::min, Long.MAX_VALUE);
    private final AtomicReference<String> lastEndpoint = 
        new AtomicReference<>("none");
    
    /**
     * Record a completed request. Called from many threads.
     * Entirely lock-free — no blocking, no synchronization.
     */
    public void recordRequest(String endpoint, long latencyMs) {
        requestCount.increment();
        totalLatency.add(latencyMs);
        maxLatency.accumulate(latencyMs);
        minLatency.accumulate(latencyMs);
        lastEndpoint.set(endpoint);
    }
    
    /**
     * Snapshot of current stats. Values are approximate (consistent 
     * individually, but not a synchronized snapshot across all fields).
     */
    public String getStats() {
        long count = requestCount.sum();
        long total = totalLatency.sum();
        long avg = count > 0 ? total / count : 0;
        
        return String.format(
            "Requests: %d | Avg: %dms | Min: %dms | Max: %dms | Last: %s",
            count, avg, minLatency.get(), maxLatency.get(), 
            lastEndpoint.get()
        );
    }
}

This statistics tracker can handle millions of concurrent updates without any locks, any blocking, or any risk of deadlock.


Key Takeaways

  1. AtomicInteger/AtomicLong — use for simple counters and numeric flags

  2. AtomicReference<T> — use for swapping immutable objects; always pair with immutable values

  3. LongAdder — use when many threads increment a counter; distributes contention across cells

  4. LongAccumulator — generalized version of LongAdder for max, min, or any associative function

  5. AtomicStampedReference — solves the ABA problem with a version stamp

  6. VarHandle — modern API for fine-grained control; use when you need relaxed access modes

  7. Golden rule: Use the highest-level, simplest API that meets your needs