Definition
- Memory barrier (fence) – a low-level instruction constraining the reordering of memory operations.
- LoadLoad – reads before the barrier complete before later reads.
- StoreStore – writes before the barrier are flushed before later writes.
- LoadStore – a read before the barrier stays before a later write.
- StoreLoad – the strongest and most expensive; flushes the store buffer so a write is globally visible before a later read.
- Release –
StoreStore + LoadStorebefore a write: publishes all prior work. - Acquire –
LoadLoad + LoadStoreafter a read: reads fresh data after seeing the flag.
Imagine clerks at National Bank of India keep private scratch pads (CPU caches) and only occasionally copy entries to the central ledger (main memory). Without rules, a clerk might reorder "mark done" and "update balance," so another clerk sees "done" before the money moved. A barrier says "finish writing the balance to the ledger before you post the done flag, and anyone reading the done flag must re-read the balance fresh."
Understanding the Concepts
Why do we need memory barriers?
Modern CPUs reorder instructions and buffer writes in per-core store buffers for speed. This reordering is invisible to a single thread (the single-thread illusion is maintained), but it is visible to other threads, which can see writes in a surprising order. Memory barriers constrain this reordering.
What are the four barrier types?
| Barrier | Prevents | Banking Example |
|---------|----------|------------------|
| LoadLoad | Later read moving before earlier read | Reading account status before reading the balance that was updated with the status |
| StoreStore | Later write moving before earlier write | Writing the "done" flag before the balance is flushed to memory |
| LoadStore | Later write moving before earlier read | Writing a derived value before reading its input |
| StoreLoad | Later read moving before earlier write | The most expensive barrier; drains the store buffer so all writes become globally visible before any subsequent read |
What are acquire and release semantics?
Release semantics guarantee that everything written before a release-store is visible to any thread that performs an acquire-load of the same variable. Acquire semantics guarantee that once you read the flag, all subsequent reads see the published data.
package com.bank.concurrency;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.math.BigDecimal;
public class AcquireReleaseDemo {
private BigDecimal amount;
private boolean ready;
private static final VarHandle READY;
static {
try {
READY = MethodHandles.lookup()
.findVarHandle(AcquireReleaseDemo.class, "ready", boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
// Publisher thread
public void publish() {
amount = new BigDecimal("50000"); // ordinary write
VarHandle.releaseFence(); // StoreStore + LoadStore
READY.setVolatile(this, true); // flag write with release semantics
}
// Consumer thread
public void consume() {
if ((boolean) READY.getVolatile(this)) { // acquire read of flag
VarHandle.acquireFence(); // LoadLoad + LoadStore
processPayment(amount); // guaranteed to see 50000
}
}
}
How does Java expose fences via VarHandle?
The VarHandle class provides five explicit fence methods. These are low-level tools for lock-free data structure authors.
| Method | Barrier Combination | Cost |
|--------|--------------------|----- |
| VarHandle.fullFence() | All four barriers | Highest |
| VarHandle.acquireFence() | LoadLoad + LoadStore | Medium |
| VarHandle.releaseFence() | StoreStore + LoadStore | Medium |
| VarHandle.loadLoadFence() | LoadLoad only | Low |
| VarHandle.storeStoreFence() | StoreStore only | Low |
When do you use fences versus volatile?
In application code, always use volatile, synchronized, or the atomic classes. They insert the correct fences automatically and express intent clearly. VarHandle fences are for lock-free data-structure authors who need minimal, precise ordering for maximum performance.
How does volatile relate to barriers?
A volatile write inserts a StoreStore barrier before the write and a StoreLoad barrier after the write. A volatile read inserts a LoadLoad barrier after the read and a LoadStore barrier after the read. Together, these establish the happens-before relationship defined by the Java Memory Model.
// volatile write is equivalent to:
StoreStore barrier;
write(field, value);
StoreLoad barrier;
// volatile read is equivalent to:
read(field);
LoadLoad barrier;
LoadStore barrier;
What are common mistakes?
Using plain field writes across threads without any barrier or volatile keyword. The reader may see stale data or a reordered sequence.
// WRONG: no barrier, consumer may see ready=true but stale amount
private BigDecimal amount;
private boolean ready;
public void publish() {
this.amount = new BigDecimal("50000");
this.ready = true; // plain write, may be reordered before amount!
}
// CORRECT: volatile ensures ordering
private BigDecimal amount;
private volatile boolean ready;
public void publish() {
this.amount = new BigDecimal("50000");
this.ready = true; // volatile write inserts StoreStore + StoreLoad
// Consumer reading ready=true is guaranteed to see amount=50000
}
Why is StoreLoad the most expensive barrier?
StoreLoad drains the entire store buffer, forcing all pending writes to become globally visible before any subsequent read. On x86, this is implemented as an MFENCE or lock prefix instruction. Other barriers are often free on x86 because the architecture already provides strong ordering (Total Store Order), but StoreLoad is always required.
What are common interview follow-up questions?
- What is the difference between a compiler barrier and a CPU barrier? A compiler barrier prevents the compiler from reordering. A CPU barrier prevents the hardware from reordering. Both are needed.
- How does the Java Memory Model relate to actual hardware barriers? The JMM defines happens-before relationships. The JVM emits the minimum hardware barriers needed to satisfy these relationships on each architecture.
- Why is x86 easier for concurrency than ARM? x86 provides Total Store Order (TSO), making most barriers free except StoreLoad. ARM has a weaker memory model, requiring more explicit barriers.
Visual Overview
CPU Store Buffer and Barriers:
Core 0 Core 1
┌──────────┐ ┌──────────┐
│ Write: │ │ Read: │
│ amount=5K│──┐ │ ready? │
│ ready=T │ │ store buffer │ │
└──────────┘ │ └──────────┘
│ ▲
▼ │
┌─────────────────────────────────────────┐
│ Main Memory │
│ amount: ??? ready: ??? │
└─────────────────────────────────────────┘
Without barrier: Core 1 sees ready=T but amount=0 (stale)
With StoreStore: amount flushed before ready
With StoreLoad: ready visible before Core 1 reads
Acquire-Release Pair:
Release fence → data published → flag written
Acquire fence → flag read → data visible fresh
Key Takeaway
A memory barrier constrains how the CPU and compiler reorder memory operations and controls when writes become visible across cores. The four types are LoadLoad, StoreStore, LoadStore, and StoreLoad, with StoreLoad being the strongest and most expensive.
The two combinations that matter most are acquire and release. Release before a write publishes all prior work. Acquire after a read guarantees fresh data. That pair is exactly what volatile and synchronized provide.
In application code I never call VarHandle fences directly; I use volatile and locks. The explicit API exists for lock-free data-structure authors.
Remember one sentence: the Java Memory Model is specified in terms of happens-before, barriers are the mechanism the JVM emits to satisfy it, and I reason in terms of volatile and locks for ordinary code.
---