Lock-Free Concurrency, CMPXCHG & VarHandle Memory Fences
In multi-threaded Java applications, traditional mutual exclusion primitives like synchronized and ReentrantLock enforce safety through OS-level thread suspension (mutexes). However, in high-throughput engines, thread parking and context switching introduce millisecond latency jitter and severe cache invalidation.
Lock-free programming abandons blocking mutexes in favor of atomic CPU hardware instructions (CMPXCHG) and fine-grained memory barriers via VarHandle.
1. Concurrency Progress Guarantees
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONCURRENCY PROGRESS GUARANTEES β
β β
β 1. BLOCKING (Pessimistic Locking - synchronized, ReentrantLock) β
β β’ Threads wait on OS mutexes. β
β β’ If holding thread is suspended or preempted, all threads stall. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2. LOCK-FREE (Optimistic CAS Loops - AtomicInteger, ConcurrentLinkedQueue) β
β β’ System-wide progress is guaranteed. β
β β’ At least one thread is guaranteed to make progress in any finite step. β
β β’ Individual threads may experience starvation under high contention. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 3. WAIT-FREE (Deterministic Bound - LMAX RingBuffer single producer) β
β β’ Strongest guarantee: EVERY thread makes progress in bounded steps. β
β β’ Zero thread suspension; zero starvation loops. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Hardware Compare-And-Swap (CAS) & The ABA Problem
Lock-free algorithms rely on the CPU's native atomic instruction CMPXCHG (Compare-and-Exchange). On x86 multi-core processors, this instruction asserts the LOCK signal on the memory bus or locks the specific cache line:
[Thread Reads Memory: Value = A]
β
βΌ
[Thread Prepares New Value: Value = B]
β
βΌ
[CPU CMPXCHG: If Memory == A, then Memory = B, else Retry]
The ABA Problem in Lock-Free Stacks
In a lock-free Treiber stack:
- Thread 1 reads top of stack: node
A(which points to nodeB). - Thread 1 is preempted by the OS scheduler before executing its CAS.
- Thread 2 pops
A, popsB, and then pushesAback onto the stack (top isA, pointing toC). - Thread 1 resumes: it checks if top is still
A. It seesA, assumes nothing has changed, and swaps top toB. - Memory Corruption: Node
Bwas already freed and unlinked, corrupting the stack structure!
Resolution: Stamped References (AtomicStampedReference)
To solve the ABA problem, the JVM pairs the memory reference with a monotonically increasing integer version tag (stamp):
import java.util.concurrent.atomic.AtomicStampedReference;
public class LockFreeStackNode<T> {
private final T value;
private final AtomicStampedReference<LockFreeStackNode<T>> nextNode;
public LockFreeStackNode(T value, LockFreeStackNode<T> next) {
this.value = value;
// Reference + Version Stamp (Starts at 0)
this.nextNode = new AtomicStampedReference<>(next, 0);
}
public boolean updateNext(LockFreeStackNode<T> expectedNext, LockFreeStackNode<T> newNext) {
int[] currentStamp = new int[1];
LockFreeStackNode<T> currentRef = nextNode.get(currentStamp);
if (currentRef != expectedNext) {
return false;
}
// Atomically compares BOTH reference and integer stamp
return nextNode.compareAndSet(
expectedNext,
newNext,
currentStamp[0],
currentStamp[0] + 1
);
}
}
3. Java 9 VarHandle Memory Ordering Modes
Historically, low-level lock-free code relied on sun.misc.Unsafe. In Java 9, java.lang.invoke.VarHandle introduced safe, standardized access to hardware memory barriers across four explicit ordering modes:
| Access Mode | Memory Fence Generated | Reordering Permissions | Hardware Use Case |
|---|---|---|---|
get() / set() (Plain) | None | Full CPU & Compiler reordering permitted | Non-volatile data, local state |
getOpaque() / setOpaque() | None (Program-order coherence only) | Prevents compiler tearing of 64-bit primitives | Non-synchronized loop counters |
getAcquire() / setRelease() | LoadLoad + LoadStore / StoreStore + LoadStore | One-way barrier; prior writes cannot move past release | High-performance lock-free publisher-subscriber |
getVolatile() / setVolatile() | Full Barrier (StoreLoad / MFENCE) | Zero reordering; enforces total sequential consistency | Concurrent state flags, multi-threaded coordinators |
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class SequenceBarrierExample {
private static final VarHandle VALUE_HANDLE;
private long sequence = 0L;
static {
try {
VALUE_HANDLE = MethodHandles.lookup()
.findVarHandle(SequenceBarrierExample.class, "sequence", long.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public void publishSequence(long newSeq) {
// Enforces StoreStore fence: All prior payload writes are flushed before updating sequence
VALUE_HANDLE.setRelease(this, newSeq);
}
public long readSequence() {
// Enforces LoadLoad fence: Ensures subsequent reads see data published with this sequence
return (long) VALUE_HANDLE.getAcquire(this);
}
}
4. Principal Architect Review Checklist
- CAS Retry Loop Safeguards: Do optimistic CAS retry loops include backoff policies or iteration limits to prevent thread starvation under extreme contention?
- ABA Vulnerability Audit: When building custom lock-free linked data structures, is node recycling guarded using version stamps (
AtomicStampedReference)? - VarHandle vs. Volatile: Are one-way barriers (
getAcquire/setRelease) used instead of fullvolatilewhen total sequential consistency across all variables is not strictly required? - Contention Isolation: Are volatile sequence numbers and counter fields padded against false sharing?
