Implementing Atomic Classes in Java Using Volatile Variables and CAS Operations
Java's atomic classes in the java.util.concurrent.atomic package are built upon two fundamental concurrency mechanisms: volatile variables and Compare-And-Swap (CAS) functions provided by the Unsafe class.
1. Characteristics of Volatile Variables
Declaring a field as volatile ensures two critical properties in a multithreaded context:
- Memory Visibility: Any write to a
volatilevariable by one thread is immediately visible to all other threads. This guarantees that subsequent reads will see the most recently updated value. - Prevention of Instruction Reordering: The compiler and CPU are prevented from reordering instructions in a way that would move operations before a write to a
volatilevariable after it, or move operations after a read from avolatilevariable before it. This establishes a happens-before relationship.
2. Ensuring Atomic Updates with CAS Functions
The atomicity of updates in these classes is guaranteed by CAS functions from the sun.misc.Unsafe class. Unsafe provides three native CAS methods:
// Atomically updates an int field if its current value matches the expected value.
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue);
// Atomically updates a long field if its current value matches the expected value.
public final native boolean compareAndSwapLong(Object obj, long offset, long expected, long newValue);
// Atomically updates an object reference field if its current reference matches the expected reference.
public final native boolean compareAndSwapObject(Object obj, long offset, Object expected, Object newValue);
Analysis of AtomicBoolean Implementation
public class AtomicBoolean implements java.io.Serializable {
// Obtains an instance of the Unsafe utility class.
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
// Memory offset for the 'value' field.
private static final long VALUE_OFFSET;
static {
try {
// Calculates and stores the memory offset of the 'value' field.
VALUE_OFFSET = UNSAFE.objectFieldOffset(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// The core state is maintained in a volatile int.
private volatile int value;
public AtomicBoolean(boolean initVal) {
// Stores true as 1, false as 0.
value = initVal ? 1 : 0;
}
public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
int exp = expectedValue ? 1 : 0;
int upd = newValue ? 1 : 0;
// Performs the atomic CAS operation using the internal int representation.
return UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, exp, upd);
}
// ... other methods
}
The AtomicBoolean class uses a volatile int field internally, mapping true to 1 and false to 0. This pattern demonstrates how atomic support for other types like char, float, or double can be implemented by converting their values to int or long to CAS operations.
2.1 Advantages of CAS
- Atomic Hardware Instruction: CAS is typically implemented as a single, uninterruptible CPU instruction. It atomically compares the current value of a variable at a memory location with an expected value and, only if they match, updates it to a new value.
- Optimistic Locking: CAS enables optimistic concurrency control. Threads attempt to update a value without acquiring a lock. If the attempt fails (because the value was changed by another thread), the thread can retry. This is often more efficient than pessimistic locking (e.g.,
synchronized), which suspends threads, especially under high contention. - Analogy to Database Operations: The semantics are similar to an atomic conditional update in SQL:
UPDATE table SET column=new_value WHERE column=expected_value. Only one concurrent transaction will succeed if they target the same row with the same condition.
2.2 Disadvantages and Limitations of CAS
- ABA Problem: A variable's value might change from A to B and back to A between the time a thread reads it and attempts a CAS. A simple CAS sees the value as unchanged (
A == A) and succeeds, potentially ignoring meaningful intermediate state changes. A common solution is to use a version number or stamp alongside the value. TheAtomicStampedReferenceclass in Java uses this approach by pairing a reference with an integer stamp. - High CPU Overhead from Spinning: If a CAS operation repeatedly fails in a loop (a "spin loop"), it can consume significant CPU resources without making progress, especially when many threads are contending for the same variable.