Java Atomic Classes: Safe Concurrent Updates Without Locks
java atomic classes: Learn how Java atomic classes like AtomicInteger and AtomicReference provide lock-free thread-safe updates, their CAS mechanics, and when to use t...
When multiple threads read and write the same field, a plain assignment like counter++ is not atomic. The read-modify-write sequence can interleave, and the final value may not reflect all updates. Java atomic classes in java.util.concurrent.atomic solve this by providing thread-safe operations on single variables without explicit synchronization.
What Java Atomic Classes Provide
The java.util.concurrent.atomic package includes AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference, and array variants like AtomicIntegerArray and AtomicReferenceArray. These classes wrap a volatile value and expose methods that perform compound operations atomically. For example, incrementAndGet() does the read, increment, and write as a single indivisible operation, while compareAndSet(expected, newValue) updates the value only if it currently equals the expected value.
These classes are designed for high-contention scenarios where locking would introduce overhead. They rely on hardware support for atomic instructions, typically a compare-and-swap (CAS) instruction on the CPU.
How Compare-and-Set Works
CAS is the core mechanism behind atomic classes. The method compareAndSet takes an expected value and a new value. If the current value matches the expected value, it is replaced with the new value and the method returns true. If not, the method returns false and the value remains unchanged. This single operation is guaranteed to be atomic by the JVM and the underlying processor.
Most compound operations, like incrementAndGet, are implemented as a loop that repeatedly calls compareAndSet until it succeeds. This is known as a spin loop. Under low contention, the loop succeeds on the first attempt. Under high contention, threads may retry several times, but the overhead is still often lower than blocking on a lock.
Practical Examples with AtomicInteger
Consider a counter shared between multiple worker threads. Using a plain int with synchronized works, but atomic classes offer a more concise and often faster alternative.
import java.util.concurrent.atomic.AtomicInteger; public class Counter { private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }
The incrementAndGet method is equivalent to count.addAndGet(1) and returns the new value. For more complex updates, updateAndGet takes a IntUnaryOperator that transforms the current value. For example, to multiply by two:
count.updateAndGet(x -> x * 2);
If you need to conditionally update based on the current value, compareAndSet gives you explicit control. The following code increments only if the current value is less than 10:
int current; do { current = count.get(); if (current >= 10) { break; } } while (!count.compareAndSet(current, current + 1));
This pattern is useful when the update depends on the value in a way that updateAndGet cannot express, such as applying a different transformation based on the current state.
AtomicReference for Object Updates
AtomicReference<V> provides the same atomic operations for object references. It is commonly used to implement non-blocking data structures or to maintain a shared mutable reference that must be updated safely.
import java.util.concurrent.atomic.AtomicReference; public class Config { private final AtomicReference<Settings> settingsRef; public Config(Settings initial) { settingsRef = new AtomicReference<>(initial); } public void update(Settings newSettings) { settingsRef.set(newSettings); } public Settings get() { return settingsRef.get(); } }
Here, set simply writes the reference, which is atomic because the reference is volatile. If you need to update the reference based on its current value, use updateAndGet or compareAndSet. For example, to swap in a new settings object only if the current one is still the original:
boolean changed = settingsRef.compareAndSet(original, newSettings);
This is useful in scenarios where you want to avoid losing an update made by another thread between your read and write.
Memory Visibility and Ordering
Atomic classes provide the same memory visibility guarantees as volatile fields. When a thread writes to an atomic variable, any subsequent read of that variable by another thread will see the updated value. This is because the underlying field is declared as volatile, and the CAS operations include the necessary memory barriers.
This visibility is critical for correctness. Without it, a thread might read a stale value even if another thread has already updated the variable. Atomic classes ensure that the value is always read from main memory, not from a thread-local cache.
Performance Considerations: Contention and Spin Loops
The main performance advantage of atomic classes is that they avoid thread suspension and context switching. When a lock is contended, the operating system may block a thread, which is expensive. In contrast, a CAS operation that fails simply retries in a tight loop. Under low contention, this is much faster than locking.
However, under very high contention, spin loops can waste CPU cycles as multiple threads repeatedly try and fail. In such cases, a lock might perform better because it lets the blocked thread yield the processor. The threshold depends on the workload, the number of cores, and the duration of the critical section. A good rule of thumb is to use atomic classes when the update is short and the contention is moderate. For long-running operations or when contention is expected to be extreme, consider a ReentrantLock or synchronized block.
Another factor is memory footprint. Atomic classes are small objects, but each one adds overhead compared to a plain field. If you have many instances, that overhead might matter. In practice, the difference is usually negligible unless you have millions of instances.
Choosing Between Atomic Classes, volatile, and synchronized
The decision depends on the operation you need to perform. A volatile field provides visibility but not atomicity. If you only read and write the field without any compound operation, volatile is sufficient. For example, a flag that indicates whether a service is running can be a volatile boolean.
If you need to perform a compound operation like increment, compare-and-set, or get-and-add, atomic classes are the natural choice. They give you the atomicity without the overhead of a lock. Use synchronized when you need to protect a larger critical section that involves multiple variables or complex logic that cannot be expressed as a single CAS operation.
| Operation | Recommended Approach |
|---|---|
| Simple read/write | volatile |
| Increment/decrement | AtomicInteger or AtomicLong |
| Compare-and-set | AtomicReference or AtomicInteger |
| Multiple-variable invariant | synchronized or ReentrantLock |
Edge Cases: ABA Problem and Atomic Field Updaters
The ABA problem occurs when a thread reads a value, another thread changes it to something else and then back, and the first thread's CAS succeeds even though the value was modified in between. This is rarely an issue for simple counters, but it can corrupt data structures that rely on reference identity. To handle it, use AtomicStampedReference or AtomicMarkableReference, which add a version stamp or boolean mark to the reference.
AtomicStampedReference<Item> ref = new AtomicStampedReference<>(item, 0); int[] stamp = new int[1]; Item current = ref.get(stamp); ref.compareAndSet(current, newItem, stamp[0], stamp[0] + 1);
Another edge case is when you want to atomically update a field of an existing object without wrapping the whole object. The AtomicReferenceFieldUpdater and AtomicIntegerFieldUpdater classes allow you to update a volatile field of a class reflectively. This is useful when you cannot change the field type to an atomic class, such as when the class is part of a public API. The updater is created statically and reused, avoiding the overhead of an atomic wrapper object.
class Node { volatile int version; static final AtomicIntegerFieldUpdater<Node> versionUpdater = AtomicIntegerFieldUpdater.newUpdater(Node.class, "version"); boolean tryIncrement() { return versionUpdater.compareAndSet(this, version, version + 1); } }
Field updaters require the field to be volatile and accessible, and they can be more error-prone because the field name is a string. Use them only when the alternative of wrapping the object in an atomic reference is impractical.