Lesson 35 of 71
VarHandle, Access Modes, and Fences
java.lang.invoke.VarHandle (Java 9, JEP 193) is a typed reference to a variable: an instance field, a static field, or an array element. It supports reads and writes in several access modes, each with a different ordering strength, plus CAS and fences. After this lesson a reader can choose the weakest mode that is still correct, which is how high-performance queues avoid the cost of full volatile writes. It builds on Atomic Classes and CAS and the barrier table in Safe Publication and final Fields.
| Mode | Read | Write | Guarantee |
|---|---|---|---|
| Plain | get |
set |
Normal field access; no ordering with other threads |
| Opaque | getOpaque |
setOpaque |
Bitwise atomic, not hoisted out of loops, coherent per variable; no ordering with other variables |
| Acquire/Release | getAcquire |
setRelease |
Later accesses stay after an acquire read; earlier accesses stay before a release write |
| Volatile | getVolatile |
setVolatile |
Same as a volatile field: total order over all volatile accesses |
Each mode includes the guarantees of the ones above it. The read-modify-write methods (compareAndSet, getAndAdd, getAndSet) have volatile semantics, and some have Acquire, Release, or Plain variants such as getAndAddRelease and weakCompareAndSetPlain.
Example 1: creating a VarHandle
MethodHandles.lookup().findVarHandle(owner, name, type) returns a handle for an instance field. It throws checked exceptions, so handles are created once in a static initializer.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class FirstVarHandle {
private int value;
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup()
.findVarHandle(FirstVarHandle.class, "value", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) {
FirstVarHandle obj = new FirstVarHandle();
VALUE.set(obj, 7);
int v = (int) VALUE.get(obj);
System.out.println("value = " + v);
}
}value = 7VarHandle methods are signature-polymorphic: the compiler takes the parameter and return types from the call site. The cast (int) is required, and the argument types must be convertible to the field type: an int passed to a long field's handle is widened, while a long passed to an int field's handle, or a cast of the result to an unrelated type such as String, throws WrongMethodTypeException. withInvokeExactBehavior() returns a handle that requires exact types. When the handle is in a static final field, the JIT compiles these calls to direct memory accesses. findStaticVarHandle does the same for static fields.
Example 2: the four access modes in one thread
In a single thread every mode gives the same result. The difference is only what other threads may observe.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class AccessModes {
private long counter;
private static final VarHandle COUNTER;
static {
try {
COUNTER = MethodHandles.lookup()
.findVarHandle(AccessModes.class, "counter", long.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) {
AccessModes a = new AccessModes();
COUNTER.set(a, 1L);
System.out.println("plain " + (long) COUNTER.get(a));
COUNTER.setOpaque(a, 2L);
System.out.println("opaque " + (long) COUNTER.getOpaque(a));
COUNTER.setRelease(a, 3L);
System.out.println("acquire " + (long) COUNTER.getAcquire(a));
COUNTER.setVolatile(a, 4L);
System.out.println("volatile " + (long) COUNTER.getVolatile(a));
}
}plain 1
opaque 2
acquire 3
volatile 4The field counter is not declared volatile, yet getVolatile and setVolatile give it volatile semantics for those accesses. A VarHandle attaches the ordering to each access instead of to the field. Plain accesses to a long may tear on 32-bit JVMs (see Visibility, Atomicity, and Reordering); opaque and stronger modes do not.
Example 3: release and acquire for message passing
A release write followed by an acquire read of the same variable that sees the written value creates the same happens-before edge as a volatile pair for that message. The cost is lower because no StoreLoad barrier is required.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class ReleaseAcquire {
private long payload; // plain
private boolean ready; // accessed with release/acquire
private static final VarHandle READY;
static {
try {
READY = MethodHandles.lookup()
.findVarHandle(ReleaseAcquire.class, "ready", boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) throws InterruptedException {
ReleaseAcquire box = new ReleaseAcquire();
Thread consumer = new Thread(() -> {
while (!(boolean) READY.getAcquire(box)) {
Thread.onSpinWait();
}
System.out.println("payload = " + box.payload);
});
consumer.start();
box.payload = 99; // plain write
READY.setRelease(box, true); // earlier writes cannot move below this
consumer.join();
}
}payload = 99What release/acquire omits is the StoreLoad order: a release write followed by an acquire read of a different variable may be reordered. Algorithms that need every thread to agree on one order of writes to different variables (Dekker-style mutual exclusion, "store my flag, then read yours") need volatile mode or a full fence. On x86 setRelease is a plain mov while setVolatile adds a locked instruction; on ARMv8 setRelease maps to stlr and getAcquire to ldar.
Example 4: CAS and getAndAdd through a VarHandle
The read-modify-write operations of the atomic classes are available on any field, which removes the wrapper object, like the field updater in Atomic Classes and CAS but without reflection checks on every call.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class HandleCounter {
private volatile long hits;
private volatile int state; // 0 = idle, 1 = running
private static final VarHandle HITS, STATE;
static {
try {
var lookup = MethodHandles.lookup();
HITS = lookup.findVarHandle(HandleCounter.class, "hits", long.class);
STATE = lookup.findVarHandle(HandleCounter.class, "state", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) throws InterruptedException {
HandleCounter c = new HandleCounter();
int[] starts = new int[4];
Thread[] threads = new Thread[4];
for (int t = 0; t < 4; t++) {
int idx = t;
threads[t] = new Thread(() -> {
if (STATE.compareAndSet(c, 0, 1)) starts[idx] = 1;
for (int i = 0; i < 100_000; i++) HITS.getAndAdd(c, 1L);
});
threads[t].start();
}
for (Thread t : threads) t.join();
System.out.println("hits = " + c.hits);
System.out.println("starts = " + (starts[0] + starts[1] + starts[2] + starts[3]));
}
}hits = 400000
starts = 1The literal 1L matches the field type exactly. HITS.getAndAdd(c, 1) also works, because the int is widened to long; the reverse, a long passed to the int handle STATE, throws WrongMethodTypeException. Declaring the fields volatile is optional here, but it keeps plain reads such as c.hits correct.
Example 5: lazySet and setRelease
AtomicLong.lazySet (Java 6) was the original "ordered write": a store that may be delayed but is never reordered with earlier stores. Since Java 9 its Javadoc defines it as having the memory effects of setRelease, and the atomic classes also expose setRelease, getAcquire, setOpaque, and getOpaque directly.
import java.util.concurrent.atomic.AtomicLong;
public class LazySetSequence {
public static void main(String[] args) throws InterruptedException {
long[] events = new long[1_000];
AtomicLong published = new AtomicLong(-1); // last index readers may read
Thread producer = new Thread(() -> {
for (int i = 0; i < events.length; i++) {
events[i] = i * 10L; // plain write of the data
published.lazySet(i); // same effect as setRelease(i)
}
});
producer.start();
producer.join();
long last = published.getAcquire();
long sum = 0;
for (int i = 0; i <= last; i++) sum += events[i];
System.out.println("published up to " + last + ", sum = " + sum);
}
}published up to 999, sum = 4995000Only the producer writes published, so release is enough: there is no competing writer whose order matters. This single-writer pattern is the basis of the queues in Ring Buffers and Lock-Free Queues.
Example 6: explicit fences
The static methods VarHandle.fullFence(), acquireFence(), and releaseFence() order plain accesses around them without naming a variable. loadLoadFence() and storeStoreFence() are narrower variants.
| Fence | Effect |
|---|---|
releaseFence() |
Loads and stores before it are not reordered with stores after it |
acquireFence() |
Loads before it are not reordered with loads and stores after it |
fullFence() |
Nothing crosses it; includes StoreLoad |
import java.lang.invoke.VarHandle;
public class Fences {
static int data;
static int flag; // plain fields only
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
data = 5;
VarHandle.releaseFence(); // data stays before flag
flag = 1;
});
writer.start();
writer.join();
int f = flag;
VarHandle.acquireFence(); // flag read stays before data read
int d = data;
System.out.println("flag = " + f + ", data = " + d);
}
}flag = 1, data = 5The output here is fixed by join(). Without join(), fences on plain fields are not enough for a spin loop: the JIT may hoist a plain read of flag out of the loop, so the loop read must be at least opaque. Fences are for code that cannot attach an access mode to the variable, for example data written by plain stores into a large structure. In most code, setRelease/getAcquire on the flag are clearer and are what the JDK uses.
Example 7: array element VarHandles
MethodHandles.arrayElementVarHandle(long[].class) returns one handle for every element of any long[]. Calls take the array and an int index. This gives atomic per-slot updates without one AtomicLong per slot.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class LatencyHistogram {
private static final VarHandle SLOT = MethodHandles.arrayElementVarHandle(long[].class);
private final long[] buckets = new long[4]; // <1ms, <10ms, <100ms, >=100ms
void record(long micros) {
int b = micros < 1_000 ? 0 : micros < 10_000 ? 1 : micros < 100_000 ? 2 : 3;
SLOT.getAndAdd(buckets, b, 1L);
}
long count(int b) { return (long) SLOT.getVolatile(buckets, b); }
public static void main(String[] args) throws InterruptedException {
LatencyHistogram h = new LatencyHistogram();
long[] samples = {500, 5_000, 50_000, 500_000};
Thread[] threads = new Thread[4];
for (int t = 0; t < 4; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < 10_000; i++) h.record(samples[i % 4]);
});
threads[t].start();
}
for (Thread t : threads) t.join();
for (int b = 0; b < 4; b++) System.out.println("bucket " + b + ": " + h.count(b));
}
}bucket 0: 10000
bucket 1: 10000
bucket 2: 10000
bucket 3: 10000AtomicLongArray is built the same way and offers the same operations with a friendlier API. MethodHandles.byteArrayViewVarHandle and byteBufferViewVarHandle view a byte[] or ByteBuffer as int or long values with a chosen byte order, which binary decoders use.
VarHandle as the replacement for Unsafe
Before Java 9, libraries such as Netty, the Disruptor, and JCTools used sun.misc.Unsafe for field offsets, CAS, and ordered writes. Unsafe has no type or bounds checks, and its memory-access methods are being deprecated for removal (JEP 471, Java 23; JEP 498 makes them warn at run time in Java 24).
Unsafe |
Replacement |
|---|---|
objectFieldOffset + compareAndSwapLong |
findVarHandle + compareAndSet |
putOrderedLong |
setRelease |
getLongVolatile / putLongVolatile |
getVolatile / setVolatile |
loadFence / storeFence / fullFence |
VarHandle.acquireFence / releaseFence / fullFence |
| Array base offset + index scale | arrayElementVarHandle |
Off-heap allocateMemory, getLong(address) |
Foreign Function and Memory API (MemorySegment, final in Java 22) |
Interview points
- A VarHandle attaches ordering to each access, not to the field; the same field can be read plain in one place and acquire in another.
- Modes from weakest to strongest: plain, opaque, acquire/release, volatile. Release/acquire omits only the StoreLoad order between different variables.
- Release/acquire is enough for single-writer message passing; Dekker-style "write mine, read yours" needs volatile or
fullFence. lazySetequalssetRelease; on x86 it is a plain store, while a volatile store needs a locked instruction.- Signature polymorphism takes types from the call site: arguments must convert to the field type (widening is allowed,
longinto aninthandle throwsWrongMethodTypeException), and the return value needs a cast. - VarHandle, plus the FFM API for off-heap memory, replaces
sun.misc.Unsafe.
Summary
findVarHandle,findStaticVarHandle, andarrayElementVarHandlecreate handles; store them instatic finalfields.- Opaque mode prevents hoisting and tearing but gives no ordering with other variables.
getAndAdd,compareAndSet, andgetAndSetwork on any field or array element.releaseFencebefore a store andacquireFenceafter a load order plain accesses.- Atomic classes expose the same weaker modes (
setRelease,getAcquire,setOpaque,getOpaque) since Java 9.
