Guide contents

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.

Java
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);
    }
}
Output
value = 7

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

Java
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));
    }
}
Output
plain    1
opaque   2
acquire  3
volatile 4

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

Java
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();
    }
}
Output
payload = 99

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

Java
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]));
    }
}
Output
hits = 400000
starts = 1

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

Java
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);
    }
}
Output
published up to 999, sum = 4995000

Only 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
Java
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);
    }
}
Output
flag = 1, data = 5

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

Java
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));
    }
}
Output
bucket 0: 10000
bucket 1: 10000
bucket 2: 10000
bucket 3: 10000

AtomicLongArray 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.
  • lazySet equals setRelease; 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, long into an int handle throws WrongMethodTypeException), and the return value needs a cast.
  • VarHandle, plus the FFM API for off-heap memory, replaces sun.misc.Unsafe.

Summary

  • findVarHandle, findStaticVarHandle, and arrayElementVarHandle create handles; store them in static final fields.
  • Opaque mode prevents hoisting and tearing but gives no ordering with other variables.
  • getAndAdd, compareAndSet, and getAndSet work on any field or array element.
  • releaseFence before a store and acquireFence after a load order plain accesses.
  • Atomic classes expose the same weaker modes (setRelease, getAcquire, setOpaque, getOpaque) since Java 9.