VarHandles and memory ordering
Purpose
Use VarHandle as a low-level, dynamically typed-by-call-site variable-access mechanism with explicit atomicity and ordering. The goal is the weakest proven sufficient protocol only when its measured benefit justifies a more fragile correctness argument.
VarHandle does not replace the JMM. Start with java-memory-model; route ABA, progress and
reclamation to lock-free-patterns.
Inspect the project's compiler release, runtime and target architectures before choosing APIs. Ordinary-field examples target Java 17+ (VarHandle itself arrived in 9); foreign-memory layout coordinates here refer to the final Java 22+ API, checked against Java 25. Earlier incubator/preview layouts differ. This skill does not authorize a JDK upgrade or preview use.
Entry gate
Prefer a volatile field, Atomic*, lock, immutable snapshot or concurrent collection unless all
hold:
- the required variable/coordinate or access mode is not expressed cleanly by a higher-level API;
- allowed outcomes and single/multiple-writer assumptions are written;
- every access path can follow one reviewed protocol;
- jcstress/model tests and target-JDK integration exist;
- assembly/performance evidence shows a decision-relevant benefit where optimization is the reason.
Protocol contract
variable type and coordinates (field/array/segment/layout):
supported read/write/update modes:
writer count and ownership:
data/invariant carried by the synchronization variable:
read and write mode on every path, including initialization/reset/error:
CAS success and failure ordering requirements:
wraparound/version/ABA and reclamation:
legal/interesting/forbidden outcomes:
progress and contention/backoff policy:
JDK/JIT/architecture measurement scope:
Access-mode lattice
Use the target JDK API specification as authoritative:
| Mode | Atomicity/order provided | Typical use |
|---|---|---|
plain get/set |
ordinary access; limited bitwise atomicity caveat for 64-bit primitives on 32-bit platforms | confined or already ordered access |
| opaque | bitwise atomic and coherently ordered for the same variable; no general cross-variable ordering | state polling/version observation where coherence alone is proven enough |
| acquire read / release write | opaque properties plus one-way ordering around matching publication/consumption | one-direction handoff |
| volatile | volatile semantics and total order among volatile operations | protocols requiring stronger global volatile order |
Opaque is not merely “atomic with no ordering”: coherent ordering of accesses to the same variable is part of its contract. Acquire/release is not a total order over all synchronization variables.
Access modes override ordering from the declaration. A VarHandle plain get of a field declared
volatile has plain mode semantics. Mixing direct volatile and weaker VarHandle accesses may be
intentional in a proven algorithm, but is a high-risk review point—not categorically illegal.
Release/acquire publication
Partial one-shot protocol: State is immutable, the holder is safely shared, exactly one
designated writer publishes once, and no path resets or overwrites data afterward. The lookup
initializer must resolve this holder's ready field as int.
private State data;
private int ready; // 0 until the sole publication
private static final VarHandle READY = /* findVarHandle */;
// Sole writer only; no concurrent callers. Reject accidental sequential reuse.
void publish(State next) {
if (ready != 0) throw new IllegalStateException("already published");
data = java.util.Objects.requireNonNull(next);
READY.setRelease(this, 1);
}
State readIfPublished() {
int observed = (int) READY.getAcquire(this);
return observed == 1 ? data : null;
}
The acquire that reads 1 matches the sole release, so prior initialization precedes subsequent data reads. It does not freeze the data: in a reusable version/data pair, the writer could overwrite data for version 2 after the reader observes version 1. A single writer and unique versions do not prevent that race. Use an atomically published immutable version+data snapshot when readers need a consistent pair, or prove an acknowledgement/ownership protocol before reuse. The plain guard is confined to the sole writer; it does not enforce multiwriter exclusion.
Atomic updates
compareAndSetreturns boolean and has volatile read/write semantics in the API contract.compareAndExchange*returns the witnessed value; success is witness equal to expected according to the API's comparison semantics.- weak CAS can fail spuriously and has plain/acquire/release/volatile variants. A retry loop handles spurious failure but does not add missing ordering.
- acquire update variants have acquire semantics for the read and plain semantics for the write; release variants have plain read and release write semantics. Confirm exact method docs.
- A failed conditional update performs no successful write/release publication. In particular, a failed release-only compare-and-exchange supplies only a plain witness read; consuming dependent data from it needs a proven acquire edge. Weak false can also be spurious.
getAndAdd, bitwise and exchange variants are only supported for applicable variable types/modes.
VarHandle access-mode methods are signature-polymorphic. Default invocation permits documented
asType-style casts, boxing/unboxing and widening; withInvokeExactBehavior() requires the exact
access-mode descriptor. Coordinates, variable type and return type must satisfy the chosen
invocation behavior; failures can be WrongMethodTypeException, ClassCastException,
or UnsupportedOperationException. Check isAccessModeSupported when building generic adapters.
Write access to read-only/final variables is unsupported for relevant handles.
CAS-loop correctness
read witness
derive candidate without irreversible side effects
attempt update with sufficient success/failure ordering
on mismatch/spurious failure: refresh, backoff/help/retry or fail
on success: publish/observe dependent state as proven
The update function may execute repeatedly. Do not put billing, I/O, callbacks or non-idempotent mutation in it. Bound or instrument retry; lock-free system progress can coexist with starvation of one thread. Handle interruption/shutdown if the loop can run indefinitely.
Fences
VarHandle provides acquire, release, full, load-load and store-store fences with precise API reordering guarantees. A fence is not a magic inter-thread handoff: the algorithm still needs a communication variable and a proof connecting writer and reader. Prefer access modes because the ordering is attached to the variable operation. Use standalone fences only for established algorithms whose proof and platform mapping are reviewed.
Architecture and generated code
Do not hard-code mov, mfence, lock add, ldar, or stlr as contracts. HotSpot C1/C2/Graal,
JDK version, CPU features, surrounding operations and compiler optimization can coalesce or select
different instructions. x86 TSO often needs fewer explicit instructions for acquire/release than
weaker architectures, but compiler ordering still matters and measured cost can be dominated by
cache-line ownership/contention.
Validate the compiled method/version, tier, inlining, surrounding barriers and target architecture. Then benchmark representative contention/topology, not only a single-thread access loop.
Proof and validation
- Draw the JMM/VarHandle edges and enumerate outcomes before code.
- Write minimal jcstress actors and results; avoid synchronization from test infrastructure.
- Add negative controls by weakening one edge and confirm the test has opportunity/sensitivity, without requiring a forbidden outcome to appear on every machine.
- Inspect compiled code when the claimed optimization depends on it.
- JMH the real access pattern across target JDKs/architectures, including contention/retry/ false-sharing counters.
- Run semantic, wraparound, multiwriter violation, cancellation and shutdown tests.
-XX:+StressGCM can perturb compiler scheduling and help stress compiler behavior; it does not
simulate all hardware/inter-thread executions or prove a protocol. Treat it as one stress mode.
Troubleshooting
stale/partial data after version observed
-> wrong mode/order, acquire did not observe intended release, plain alternate path, mutation
CAS loop CPU high
-> contention, false sharing, spurious/mismatch rate, no backoff/help, stalled owner
works on one architecture/JIT
-> missing language proof or codegen assumption; jcstress and exact compiled method
WrongMethodType/ClassCast
-> coordinate/variable/call-site descriptor mismatch
UnsupportedOperationException
-> factory/type/read-only handle does not support selected mode
rare corruption after wrap/reuse
-> ABA/version overflow/reclamation/lifetime protocol
Anti-patterns
| Anti-pattern | Failure | Better approach | Narrow exception |
|---|---|---|---|
| Weaker mode because x86 instruction is cheaper | nonportable/unproven | derive mode from outcomes, then measure | architecture-specific internal with proof |
| Opaque described as plain atomic | coherence contract missed | quote exact VarHandle API | |
| Retry loop makes weak CAS ordered | spurious retry != fence | choose sufficient CAS variant | |
| Volatile declaration plus plain VH assumed volatile | access mode overrides declaration | audit each path | |
| Fence without carrier protocol | no communication edge | release/acquire variable or proven algorithm | |
| Single-writer protocol undocumented | future writer corrupts silently | enforce/document owner or serialize writers |
Definition of done
- Higher-level alternatives were rejected for stated reasons.
- Coordinates/types/supported modes and every access path are inventoried.
- Writer count, publication data, CAS success/failure, ABA/wrap/reclamation are proven.
- Outcomes plus jcstress positive/negative controls exist.
- Codegen and JMH claims are scoped to exact JDK/JIT/architecture/topology.
- Retry/progress/contention and lifecycle failure modes are observable and tested.
References
- Access-mode selection and API matrix — when choosing update modes or adapting coordinates/types.
- Proving ordering and measuring cost — when defining litmus outcomes or validating a codegen/performance claim.
- Java 25
VarHandle - JLS 17.4
- OpenJDK jcstress