Lock-Free Programming - Java (Part 2)
Memory Models and the Happens-Before Guarantee
Before we can write lock-free code, we need to understand a mind-bending truth:
The code you write is NOT the code that runs.
Compilers reorder your instructions. CPUs execute them out of order. Caches hold stale data. And all of this is invisible to you — until you add threads. Then it becomes a minefield.
This article explains why this happens and how Java's Memory Model gives you tools to tame the chaos.
Why Memory Isn't What You Think
When you write Java code, you probably imagine something like this:
Your Code → CPU → Memory → Done
The reality is far more complex:
Each CPU core has its own cache (a fast, local copy of memory) and a store buffer (a queue of pending writes). When Thread 1 writes a value, it doesn't go to main memory immediately. It might sit in Thread 1's store buffer or cache — invisible to Thread 2.
A Scary Example
public class VisibilityBug {
private boolean ready = false;
private int answer = 0;
// Thread 1 runs this
public void writer() {
answer = 42; // Write 1
ready = true; // Write 2
}
// Thread 2 runs this
public void reader() {
if (ready) { // Read 1
System.out.println(answer); // Read 2
}
}
}
Quiz: If Thread 2 sees ready == trueWill it always print 42?
Answer: No! It might print 0. Here's why:
Two things went wrong:
Write reordering: The CPU decided it was faster to write
readybeforeanswerCache staleness: Thread 2's cache still had the old value of
answer
This is perfectly legal behavior for modern CPUs. They guarantee nothing about the order other cores see your writes — unless you tell them to.
Three Villains of Shared Memory
Villain 1: Compiler Reordering
The Java compiler (and the JIT compiler at runtime) can rearrange your code for optimization:
// What you wrote:
x = 1;
y = 2;
// What might execute:
y = 2;
x = 1;
This is safe in a single thread (the end result is the same). But with multiple threads reading x and y, the order matters!
Villain 2: CPU Instruction Reordering
Even if the compiler preserves your order, the CPU itself can execute instructions out of order. Modern CPUs have deep pipelines and will rearrange instructions to keep all execution units busy.
Villain 3: Cache Incoherence
Each core's cache can hold different versions of the same variable. Without explicit synchronization, there's no guarantee that one core sees another core's writes.
The Java Memory Model (JMM) to the Rescue
Java doesn't leave you at the mercy of hardware quirks. The Java Memory Model (defined in JSR 133, part of the language specification since Java 5) provides a set of rules that guarantee when writes by one thread become visible to reads by another thread.
The key concept is the happens-before relationship.
What Is Happens-Before?
Happens-before is a guarantee: if action A happens-before action B, then:
A's results are visible to B
A is ordered before B (no reordering can swap them)
Think of it as a contract between you and the JVM:
"If you use these specific constructs, I promise your writes will be visible in the right order."
The Happens-Before Rules
Here are the key rules that establish happens-before relationships:
Volatile: Your First Memory Fence
The volatile keyword is the simplest way to create happens-before relationships between threads.
public class VisibilityFixed {
private volatile boolean ready = false; // volatile!
private int answer = 0;
// Thread 1
public void writer() {
answer = 42; // Write 1
ready = true; // Write 2 (volatile write)
}
// Thread 2
public void reader() {
if (ready) { // Read 1 (volatile read)
System.out.println(answer); // Guaranteed to print 42!
}
}
}
Why Does This Work?
A volatile write acts like a barrier:
All writes before the volatile write are flushed to main memory
The volatile write itself is immediately visible to other threads
A volatile read acts like a refresh:
The CPU invalidates its cache and reads fresh values from main memory
All reads after the volatile read see the latest values
What Volatile Does NOT Do
volatile ensures visibility and ordering, but NOT atomicity of compound operations.
private volatile int count = 0;
// THIS IS STILL BROKEN!
public void increment() {
count++; // read + add + write — NOT atomic even with volatile!
}
The count++ is three operations. volatile ensures each individual read and write is visible to other threads, but it doesn't prevent two threads from reading the same value before writing. For that, you need CAS operations.
Understanding Memory Barriers (Fences)
Under the hood, volatile and synchronized work by inserting memory barriers (also called fences) into the instruction stream. A memory barrier is a CPU instruction that says: "Don't reorder anything past this point."
There are four types:
| Barrier Type | What It Prevents |
|---|---|
| LoadLoad | Ensures loads before the barrier are completed before loads after it |
| StoreStore | Ensures stores before the barrier are complete before stores after it |
| LoadStore | Ensures loads before the barrier are complete before stores after it |
| StoreLoad | Ensures stores before the barrier are completed before loads after it (most expensive!) |
You rarely need to think about individual fence types in Java — volatile and synchronized handle them for you. But understanding that they exist helps you appreciate the cost: memory barriers force the CPU to give up some of its optimization tricks, which is why they're not free.
Practical Example: A Safe Publication Pattern
One of the most common bugs in concurrent Java is unsafe publication — exposing an object to another thread before it's fully constructed.
The Bug
public class Holder {
private int value;
public Holder(int n) {
this.value = n;
}
public int getValue() {
return value;
}
}
// Shared between threads
public class Unsafe {
private Holder holder; // NOT volatile
public void initialize() {
holder = new Holder(42);
}
public void use() {
if (holder != null) {
// Can print 0! The other thread might see a
// partially-constructed Holder!
System.out.println(holder.getValue());
}
}
}
The Fix
public class Safe {
private volatile Holder holder; // volatile!
public void initialize() {
holder = new Holder(42);
}
public void use() {
Holder h = holder; // single volatile read
if (h != null) {
System.out.println(h.getValue()); // Always 42 ✅
}
}
}
The volatile write to holder ensures that all writes during the Holder's constructor (including this.value = n) are visible to any thread that reads holder.
How This Connects to Lock-Free Programming
Everything in lock-free programming depends on the Java Memory Model:
The atomic classes (AtomicInteger, AtomicReference, etc.) that power lock-free code provides:
Volatile-like visibility — writes and reads have happens-before semantics
CAS operations — atomic read-modify-write in a single hardware instruction
Without the JMM's happens-before guarantees, none of our lock-free algorithms would be correct. The CAS operation itself creates a happens-before edge — a successful compareAndSet A variable guarantees that all writes before the CAS are visible to any thread that subsequently reads that variable.
Key Takeaways
CPUs and compilers reorder operations for performance — this is invisible in single-threaded code, but deadly in multi-threaded code
Each CPU core has its own cache — writes by one core may not be immediately visible to other cores
The Java Memory Model defines when writes become visible through happens-before relationships
volatileensures visibility and ordering, but NOT atomicity of compound operations likecount++Memory barriers are the low-level mechanism that enforces ordering —
volatileandsynchronizedinsert them for youAtomic classes combine volatile semantics with CAS operations — this is the foundation of lock-free code