Explain VarHandle's access modes — plain, opaque, acquire/release, and volatile — and how they differ in memory-ordering strength.
answer
- Strength ladder: plain < opaque < acquire/release < volatile
- Opaque = atomic + per-variable coherence/progress, no cross-var order
- Acquire/release = one-directional publish→consume happens-before
- Volatile = full bidirectional, globally ordered, most expensive
- weakCompareAndSet may fail spuriously → use in a loop; pick weakest correct mode
basics
~20 sVarHandle lets you pick how strongly a read or write is ordered. Plain is a normal access with no guarantees; opaque keeps a single variable's accesses coherent and atomic; acquire/release pair up so a release write is seen by a later acquire read; volatile is the strongest, full ordering. You pick the weakest mode that is still correct, for speed.
solid answer
~50 sVarHandle exposes a ladder of access modes that trade ordering strength for performance. Plain (get/set) is an ordinary read/write with no inter-thread ordering or atomicity guarantee beyond the language baseline. Opaque (getOpaque/setOpaque) guarantees the access is atomic and that accesses to the same variable are coherently ordered, but imposes no ordering relative to other variables. Acquire/release (getAcquire/setRelease) gives one-directional happens-before: a setRelease write is visible to, and ordered before, any subsequent getAcquire read of the same variable that observes it — the cheap publication idiom. Volatile (getVolatile/setVolatile) is the strongest: full bidirectional ordering equivalent to a volatile field, with a global ordering of volatile accesses. The atomic ops (compareAndSet, getAndAdd) default to volatile-strength ordering but also have acquire/release/plain variants. The engineering point: use the weakest mode that remains correct, because weaker modes permit more hardware/compiler reordering and run faster on relaxed-memory CPUs.
code
java · 36 linesimport java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
class Publication {
private int payload; // written plain
private volatile boolean ready;
private static final VarHandle READY;
static {
try {
READY = MethodHandles.lookup()
.findVarHandle(Publication.class, "ready", boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
void produce(int v) {
payload = v; // plain store of the data
READY.setRelease(this, true); // release: publishes payload + flag
}
int consume() {
// acquire: if we see the release, we see everything before it
if ((boolean) READY.getAcquire(this)) {
return payload; // safe: ordered after the release
}
return -1;
}
// weakCompareAndSet must run in a retry loop (spurious failure allowed)
void setReadyViaCas() {
while (!READY.weakCompareAndSet(this, false, true)) {
// retry: a false return may be spurious, not a real mismatch
}
}
}go deeper
Not expected to know access modes; knowing volatile exists and gives visibility is enough at this level.
Can state that VarHandle offers stronger and weaker access modes and that volatile is the strongest; details of opaque/acquire-release are aspirational.
Explains the ladder, what acquire/release publication buys over volatile, and that weakCompareAndSet can fail spuriously and needs a loop.
Reasons precisely about each mode's guarantees, maps them to hardware barriers and the JMM, and applies 'weakest correct mode' to design or review a lock-free primitive.
## Why memory ordering is even a thing Modern CPUs and compilers **reorder** memory operations and keep values in per-core caches/registers for speed. Without rules, a write by thread A may become visible to thread B late, or out of program order. The **Java Memory Model (JMM)** defines, via the **happens-before** relation, when one thread's writes are guaranteed visible to another. A **memory barrier** (or fence) is the hardware/compiler mechanism that restricts reordering to enforce such guarantees — but barriers cost performance. The art of lock-free programming is using the **cheapest barrier that is still correct**. Before VarHandle, your field-ordering choices in pure Java were coarse: a field was either plain or `volatile` (all-or-nothing strong ordering). VarHandle exposes a **ladder of access modes** so you can ask for exactly the strength you need. ## The ladder, weakest to strongest ### 1. Plain — `get` / `set` Behaves like a normal field read/write. The only guarantee is the JMM baseline: no tearing for most types, but **no inter-thread ordering and no visibility guarantee** — another thread might never see the write, or see it reordered. Use when the variable is thread-confined or ordering is established by something else. (Note: even plain access to a `long`/`double` is not guaranteed atomic unless the variable is otherwise made so; opaque and stronger guarantee atomicity.) ### 2. Opaque — `getOpaque` / `setOpaque` Adds two things over plain: **bitwise atomicity** (the value is read/written as a whole, no tearing) and **per-variable coherence/progress** — accesses to *that same variable* are not reordered with each other and a writer's updates will *eventually* become visible (a spinning reader will see them). What opaque does **not** give is any ordering relative to *other* variables: a release of opaque writes to different variables can be reordered. Think "this one variable behaves sanely and makes progress, but don't rely on it to publish anything else." Useful for things like a progress/cancellation flag where you only care about that flag. ### 3. Acquire / Release — `getAcquire` / `setRelease` This is the **one-directional (half) barrier** pair, the workhorse of efficient lock-free code. - A **`setRelease(x)`** ensures that **all writes the thread did before it** are visible to any thread that later does a `getAcquire` and observes `x`. (Release = "everything before me is published with me.") - A **`getAcquire`** ensures that **reads the thread does after it** see the published state, and are not hoisted above the acquire. (Acquire = "everything after me sees what the matching release published.") Together they form a happens-before edge **only in the publish→consume direction**, which is cheaper than full volatile because it constrains reordering in just one direction. This is the classic *safe publication* idiom: write the payload with plain stores, then `setRelease` a flag/reference; the reader `getAcquire`s the flag and then safely reads the payload. ### 4. Volatile — `getVolatile` / `setVolatile` The **strongest** mode, equivalent to reading/writing a `volatile` field. It gives full **bidirectional** ordering at the access and participates in a **total order of all volatile (sequentially-consistent) accesses**, so independent volatile variables can't be observed in inconsistent orders. This is what you need when you require global ordering between *different* variables. It is also the most expensive (full fences on relaxed architectures). ## Atomic operations and their modes The read-modify-write atomics also come in ordering flavors: - `compareAndSet` — full **volatile-strength** ordering (the default "strong CAS"). - `compareAndExchange` — returns the witnessed value; has `Acquire`/`Release` variants for one-directional ordering. - `weakCompareAndSet` — may **fail spuriously** even when the value matches, so it is only used inside a **retry loop**; in exchange it can be cheaper on some hardware. There are plain/acquire/release variants too. - `getAndAdd`, `getAndSet`, `getAndBitwiseOr`, etc., similarly default to volatile-strength with weaker variants. "Spurious failure" matters: with `weakCompareAndSet` you must loop, because a `false` return does **not** mean the expected value was wrong. ## The mental model and the rule Ordering strength: **plain < opaque < acquire/release < volatile**. Each step up adds guarantees and adds cost. The principle for experts: **choose the weakest access mode that keeps your algorithm correct.** Over-using `volatile`-strength is correct but slow on weakly-ordered CPUs (ARM, POWER); using too weak a mode is a subtle, rare, hard-to-reproduce data race. This is genuinely hard to reason about — which is why these modes are reserved for carefully reviewed concurrency primitives, not everyday code. ## A concrete publication example ```java class Holder { private int data; // plain payload private boolean ready; // published via release/acquire private static final VarHandle READY; static { try { READY = MethodHandles.lookup() .findVarHandle(Holder.class, "ready", boolean.class); } catch (ReflectiveOperationException e) { throw new Error(e); } } void publish(int v) { data = v; // 1: plain write of payload READY.setRelease(this, true); // 2: release — publishes (1) } int consume() { if ((boolean) READY.getAcquire(this)) // acquire — pairs with release return data; // guaranteed to see the published payload return -1; } } ``` Here acquire/release gives exactly the visibility needed without the heavier full-volatile fences. ## Summary VarHandle's access modes form a strength ladder — **plain** (no ordering), **opaque** (atomic + per-variable coherence/progress), **acquire/release** (one-directional publish→consume happens-before), and **volatile** (full bidirectional, globally ordered). They let an expert request precisely the barrier strength an algorithm needs, with the guiding rule to pick the weakest correct mode for performance, plus matching ordering variants on the atomic read-modify-write operations.
- When would you choose acquire/release over full volatile access?For the publish/consume idiom — write a payload, then setRelease a flag/reference; a reader getAcquires the flag and then reads the payload. You only need ordering in the publish→consume direction, so the cheaper one-directional barriers suffice and avoid full-volatile fence cost on weakly-ordered hardware.
- Why must weakCompareAndSet be used inside a loop?It is allowed to fail spuriously — returning false even when the variable holds the expected value — to map efficiently onto load-linked/store-conditional hardware. So a false result does not imply a real mismatch; you retry until it succeeds (or re-check the expected value).
saying these in an interview costs you the question
- Saying acquire/release gives full bidirectional ordering — it is one-directional
- Treating opaque as establishing ordering between different variables — it does not
- Claiming weakCompareAndSet's false return means the expected value was wrong — it can be spurious
- Believing plain access guarantees cross-thread visibility — it does not
- Assuming stronger modes are always 'safer to just use' without acknowledging the performance cost on relaxed CPUs