Back to Safe Publication & Memory Model
Q5Question 5 of 5

What are memory barriers?

Key Concepts
  • A memory barrier constrains the order in which the CPU and compiler reorder memory operations.
  • LoadLoad: reads before the barrier stay before later reads. StoreStore: writes before stay before later writes.
  • LoadStore: a read stays before a later write. StoreLoad: a write is visible before a later read (most expensive).
  • Release semantics: everything before a write is published before the flag write.
  • Acquire semantics: once a flag read succeeds, all subsequent reads see the published data.
  • Java exposes VarHandle.fullFence, acquireFence, releaseFence, loadLoadFence, storeStoreFence.
  • In application code, use volatile and synchronized; VarHandle fences are for lock-free data-structure authors.

Definition

Memory barriers stop the CPU and compiler from reordering memory operations and control cross-core visibility; release publishes all prior writes before a flag write, acquire sees all prior writes after reading the flag, and Java exposes them via VarHandle fences.
  • 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 + LoadStore before a write: publishes all prior work.
  • Acquire – LoadLoad + LoadStore after 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.

JAVA
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.

JAVA
// 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.

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate