Lock-Free Programming - Java (Part 4)
Atomic Variables in Java — Your Lock-Free Toolbox
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[]? AnAtomicIntegerArraystores the values in a contiguous block, which is better for cache performance. AnAtomicInteger[]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/AtomicReferencefor most use cases. Reach forVarHandlewhen 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
AtomicInteger/AtomicLong— use for simple counters and numeric flagsAtomicReference<T>— use for swapping immutable objects; always pair with immutable valuesLongAdder— use when many threads increment a counter; distributes contention across cellsLongAccumulator— generalized version of LongAdder for max, min, or any associative functionAtomicStampedReference— solves the ABA problem with a version stampVarHandle— modern API for fine-grained control; use when you need relaxed access modesGolden rule: Use the highest-level, simplest API that meets your needs