A variable handle is a reference to a variable, or to a family of variables, including static fields, nonstatic fields, array elements, or components of an off-heap data structure. The concept of variable handles is similar to method handles. Variable handles are represented using the class java.lang.invoke.VarHandle. With variable handles, you can perform different operations on variables called access modes. All supported access modes are defined in the enum VarHandle.AccessMode.
Creating Variable Handles
VarHandle objects can be created using factory methods in the java.lang.invoke.MethodHandles.Lookup class. First you can get an instance of MethodHandles.Lookup using the method MethodHandles.lookup(), then you can use its methods to create VarHandle objects.
findStaticVarHandle
The method VarHandle findStaticVarHandle(Class<?> decl, String name, Class<?> type) returns a VarHandle that accesses a static field name of the given type declared in a type decl. In Listing 8-1, I create a VarHandle that accesses the static int field staticVar declared in the class HandleTarget.
Listing 8-1. Example of findStaticVarHandle()
MethodHandles.lookup()
.findStaticVarHandle(HandleTarget.class, "staticVar", int.class);
findVarHandle
The method VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) returns a VarHandle that accesses a nonstatic field name of the given type in a type recv. In Listing 8-2, I create a VarHandle that accesses the nonstatic int field count declared in the class HandleTarget.
Listing 8-2. Example of findVarHandle()
MethodHandles.lookup()
.findVarHandle(HandleTarget.class, "count", int.class);
unreflectVarHandle
The method VarHandle unreflectVarHandle(Field f) creates a VarHandle from a java.lang.reflect.Field object. In Listing 8-3, I create a VarHandle using the Field object created from the method getDeclaredField() of HandleTarget.class.
Listing 8-3. Example of unreflectVarHandle()
MethodHandles
.lookup()
.unreflectVarHandle(HandleTarget.class.getDeclaredField("count"));
Access Modes
To understand different variable access modes, you first need to understand memory ordering.
Memory Ordering
The access modes of variable handles are compatible with C/C++11 atomics ( http://en.cppreference.com/w/cpp/atomic ). Table 8-1 shows the different memory orderings supported by access modes.
Table 8-1. Memory Orderings
Memory Ordering | Access | Description |
---|---|---|
plain | read/write | The same memory semantics as nonvolatile, which has no special memory ordering effects with respect to other threads. Only atomic for references and for primitive values of, at most, 32 bits. |
volatile | read/write | The same memory semantics as Java volatile. |
opaque | read/write | No assurance of memory ordering effects with respect to other threads. Only atomic with respect to accesses to the same variable. |
acquire | read | Ensures that subsequent loads and stores are not reordered before this access; compatible with C/C++11 memory_order_acquire ordering. |
release | write | Ensures that prior loads and stores are not reordered after this access; compatible with C/C++11 memory_order_release ordering. |
Note
For more details about C/C++11 acquire and release memory ordering, see http://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering .
These memory orderings are used as the suffix of access modes to specify the memory ordering effects of these modes. For example, GET_AND_ADD_ACQUIRE has the memory ordering acquire for the read access.
VarHandle Methods
Thirty-one access modes are defined in the enum VarHandle.AccessMode. For each access mode, there is a corresponding method in VarHandle that you need to use with this access mode. For example, for the access mode GET_AND_ADD, the method in VarHandle is getAndAdd().
Signature Polymorphic
The methods for access modes in VarHandle are signature polymorphic. Even though all these methods have the same signature of Object methodName(Object... args), a runtime check is done to make sure the runtime types of arguments match the requirements of the access modes. If the argument matching fails, JVM throws a java.lang.invoke.WrongMethodTypeException. Because the compiler doesn’t check the arity and types of arguments statically, it’s the developers’ responsibility to make sure that the correct number of arguments with the correct types are passed when invoking those methods.
The list of arguments consists of between zero and many objects of coordinate types and optional arguments that are required by different access modes:
These coordinate types are used to locate the variable you want to access. For example, when accessing an element in the array, the coordinate types are the type of the array and the type of the array index. You can use the method List<Class<?>> coordinateTypes() of VarHandle to get the list of coordinate types.
Different access modes may require extra arguments. For example, the method compareAndSet() requires the expected value to compare and the new value to set.
These access modes can be grouped into five categories: read access, write access, atomic update, numeric atomic update, and bitwise atomic update. The class HandleTarget in Listing 8-4 contains different static variables for testing. I’ll use these variables in following sections.
Listing 8-4. HandleTarget for Testing
public class HandleTarget {
public int count = 1;
public String[] names = new String[]{"Alex", "Bob", "David"};
public byte[] data = new byte[]{1, 0, 0, 0, 1, 0, 0, 0};
public ByteBuffer dataBuffer = ByteBuffer.wrap(this.data);
}
Now let’s discuss all five categories of methods.
Read Access
This category includes the get(), getVolatile(), getOpaque(), and getAcquire() methods with memory ordering effects specified by the suffix. The method get() has th e plain memory ordering effect (see Table 8-1). Listing 8-5 shows the usage of different read access methods. The variable handle varHandle references the variable count in the class HandleTarget of Listing 8-4. Because the test case is not multithreaded, all these read access modes return the same result.
Listing 8-5. Read Access
public class VarHandleTest {
private HandleTarget handleTarget = new HandleTarget();
private VarHandle varHandle;
@Before
public void setUp() throws Exception {
this.handleTarget = new HandleTarget();
this.varHandle = MethodHandles
.lookup()
.findVarHandle(HandleTarget.class, "count", int.class);
}
@Test
public void testGet() throws Exception {
assertEquals(1, this.varHandle.get(this.handleTarget));
assertEquals(1, this.varHandle.getVolatile(this.handleTarget));
assertEquals(1, this.varHandle.getOpaque(this.handleTarget));
assertEquals(1, this.varHandle.getAcquire(this.handleTarget));
}
}
All these read access methods only require one parameter, which is the target object that contains this variable. The class HandleTarget is the only coordinate type.
Write Access
This category includes the set(), setVolatile(), setOpaque(), and setRelease() methods with memory ordering effects specified by the suffix. The method set() has the plain memory ordering effect (see Table 8-1). Listing 8-6 shows the usage of different write access methods.
Listing 8-6. Write Access
@Test
public void testSet() throws Exception {
final int newValue = 2;
this.varHandle.set(this.handleTarget, newValue);
assertEquals(newValue, this.varHandle.get(this.handleTarget));
this.varHandle.setVolatile(this.handleTarget, newValue + 1);
assertEquals(newValue + 1, this.varHandle.get(this.handleTarget));
this.varHandle.setOpaque(this.handleTarget, newValue + 2);
assertEquals(newValue + 2, this.varHandle.get(this.handleTarget));
this.varHandle.setRelease(this.handleTarget, newValue + 3);
assertEquals(newValue + 3, this.varHandle.get(this.handleTarget));
}
All these write access methods takes two parameters. The first parameter is the target object, the second is the new value to set.
Atomic Update
This category includes methods from four subcategories with memory ordering effects specified by the suffix. All the methods perform both read and write access to a variable, which may have different memory ordering effects for read and write access.
The method compareAndSet() atomically sets the value of a variable if the variable’s current value equals the expected value with the volatile memory ordering for both read and write access. The returned boolean value indicates if the operation successfully updates the value.
The weakCompareAndSet(), weakCompareAndSetPlain(), weakCompareAndSetAcquire(), and weakCompareAndSetRelease() methods could atomically set the value of a variable if the variable’s current value equals the expected value. The returned Boolean value indicates if the operation successfully updated the value. This operation may fail even if the variable’s current value does match the expected value. This is why they are called weak operations.
The compareAndExchange(), compareAndExchangeAcquire(), and compareAndExchangeRelease() methods atomically set the value of a variable if the variable’s current value is equal to the expected value. The return value is the current value, which will be the same as the expected value if the operation is successful.
The getAndSet(), getAndSetAcquire(), and getAndSetRelease() methods atomically set the value of a variable to the new value and return the variable’s previous value.
Table 8-2 shows each method’s memory ordering for read and write access.
Table 8-2. Memory Ordering for Read and Write Access of Atomic Update Methods
Method | Read | Write |
---|---|---|
compareAndSet() | volatile | volatile |
weakCompareAndSet() | volatile | volatile |
weakCompareAndSetPlain() | plain | plain |
weakCompareAndSetAcquire() | acquire | plain |
weakCompareAndSetRelease() | plain | release |
compareAndExchange() | volatile | volatile |
compareAndExchangeAcquire() | acquire | plain |
compareAndExchangeRelease() | plain | release |
getAndSet() | volatile | volatile |
getAndSetAcquire() | acquire | plain |
getAndSetRelease() | plain | release |
Listing 8-7 shows how to use the different atomic update methods.
Listing 8-7. Atomic Update Methods
@Test
public void testAtomicUpdate() throws Exception {
final int expectedValue = 1;
final int newValue = 2;
assertEquals(true,
this.varHandle.compareAndSet(this.handleTarget, expectedValue, newValue));
assertEquals(newValue,
this.varHandle.compareAndExchange(this.handleTarget, newValue, newValue + 1));
assertEquals(newValue + 1, this.varHandle.getAndSet(this.handleTarget, newValue + 2));
}
Numeric Atomic Update
The getAndAdd(), getAndAddAcquire(), and getAndAddRelease() methods atomically add the value to the current value of a variable a nd return the variable’s previous value. Table 8-3 shows the memory ordering for read and write access of these methods.
Table 8-3. Memory Ordering for Read and Write Access of Numeric Atomic Update Methods
Method | Read | Write |
---|---|---|
getAndAdd() | volatile | volatile |
getAndAddAcquire() | acquire | plain |
getAndAddRelease() | plain | release |
Listing 8-8 shows how to use different numeric atomic update methods.
Listing 8-8. Numeric Atomic Update Methods
@Test
public void testNumericAtomicUpdate() throws Exception {
final int expectedValue = 1;
assertEquals(expectedValue,
this.varHandle.getAndAdd(this.handleTarget, 1));
assertEquals(expectedValue + 1,
this.varHandle.getAndAddAcquire(this.handleTarget, 1));
assertEquals(expectedValue + 2,
this.varHandle.getAndAddRelease(this.handleTarget, 1));
}
Bitwise Atomic Update
The getAndBitwiseAnd(), getAndBitwiseAndAcquire(), getAndBitwiseAndRelease(), getAndBitwiseOr(), getAndBitwiseOrAcquire(), getAndBitwiseOrRelease(), getAndBitwiseXor(), getAndBitwiseXorAcquire(), and getAndBitwiseXorRelease() methods atomically set the value of a variable to the result of a bitwise AND/OR/XOR between the variable’s current value and the mask value and return the variable’s previous value. Table 8-4 shows the memory ordering for read and write access of these methods.
Table 8-4. Memory Ordering for Read and Write Access of Bitwise Atomic Update Methods
Method | Read | Write |
---|---|---|
getAndBitwiseAnd() | volatile | volatile |
getAndBitwiseOr() | volatile | volatile |
getAndBitwiseXor() | volatile | volatile |
getAndBitwiseAndAcquire() | acquire | plain |
getAndBitwiseOrAcquire() | acquire | plain |
getAndBitwiseXorAcquire() | acquire | plain |
getAndBitwiseAndRelease() | plain | release |
getAndBitwiseOrRelease() | plain | release |
getAndBitwiseXorRelease() | plain | release |
Listing 8-9 shows how to use different bitwise numeric atomic update methods.
Listing 8-9. Bitwise Atomic Update Methods
@Test
public void testBitwiseAtomicUpdate() throws Exception {
final int mask = 1;
assertEquals(1, this.varHandle.getAndBitwiseAnd(this.handleTarget, mask));
assertEquals(1, this.varHandle.get(this.handleTarget));
assertEquals(1, this.varHandle.getAndBitwiseOr(this.handleTarget, mask));
assertEquals(1, this.varHandle.get(this.handleTarget));
assertEquals(1, this.varHandle.getAndBitwiseXor(this.handleTarget, mask));
assertEquals(0, this.varHandle.get(this.handleTarget));
}
Arrays
VarHandles can also be used to access individual elements in an array. You can create a VarHandle that accesses array elements using the method MethodHandles.arrayElementVarHandle(Class<?> arrayClass).
In Listing 8-10, I create a VarHandle that accesses String[] arrays. The method compareAndSet() updates the first element in the array to the new value. You need to provide the array, index, expected value, and new value when invoking this method.
Listing 8-10. VarHandle for Accessing Array Elements
@Test
public void testArray() throws Exception {
final VarHandle arrayElementHandle = MethodHandles
.arrayElementVarHandle(String[].class);
assertEquals(true,
arrayElementHandle.compareAndSet(
this.handleTarget.names, 0, "Alex", "Alex_new"));
assertEquals("Alex_new", this.handleTarget.names[0]);
}
byte[] and ByteBuffer Views
VarHandle allows you to view a byte array or ByteBuffer as an array of different primitive types, for example, int[] or long[]. You can use the MethodHandles.byteArrayViewVarHandle() or MethodHandles.byteBufferViewVarHandle() methods to create views of byte[] or ByteBuffer, respectively.
In order for you to be able to use the byteArrayViewVarHandle() method to create a VarHandle, the first parameter must be the array class you use to view the byte array. The element type can be short, char, int, long, float, and double. The second parameter is the byte order with type java.nio.ByteOrder, and it can be BIG_ENDIAN or LITTLE_ENDIAN. In Listing 8-11, I create a VarHandle by viewing the byte array as int[] with the byte order set to BIG_ENDIAN. Then I use the method get() to get the int value starting at the specified index in the original byte array. The maximum value of the index is the size of the byte array minus the byte size of the element type. For the VarHandle in Listing 8-11, the byte size of int is 4, so the maximum value of the index is 8 – 4 = 4. Using an index value greater than 4 will result in an IndexOutOfBoundsException.
Listing 8-11. Byte Array View
@Test
public void testByteArrayView() throws Exception {
final VarHandle varHandle = MethodHandles
.byteArrayViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN);
final byte[] data = this.handleTarget.data;
assertEquals(16777216, varHandle.get(data, 0));
assertEquals(1, varHandle.get(data, 1));
assertEquals(256, varHandle.get(data, 2));
assertEquals(65536, varHandle.get(data, 3));
assertEquals(16777216, varHandle.get(data, 4));
}
Listing 8-12 uses the method byteBufferViewVarHandle() to create a VarHandle. The code has the same result as Listing 8-11.
Listing 8-12. ByteBuffer View
@Test
public void testByteBufferView() throws Exception {
final VarHandle varHandle = MethodHandles
.byteBufferViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN);
final ByteBuffer dataBuffer = this.handleTarget.dataBuffer;
assertEquals(16777216, varHandle.get(dataBuffer, 0));
assertEquals(1, varHandle.get(dataBuffer, 1));
assertEquals(256, varHandle.get(dataBuffer, 2));
assertEquals(65536, varHandle.get(dataBuffer, 3));
assertEquals(16777216, varHandle.get(dataBuffer, 4));
}
Memory Fence
VarHandle also adds support for memory fencing that controls memory ordering in align with the C/C++11 atomic_thread_fence ( http://en.cppreference.com/w/cpp/atomic/atomic_thread_fence ). A fence ensures that loads and/or stores before the fence will not be reordered with loads and/or stores after the fence. There are five different static methods in VarHandle to create different types of fences that control what operations are not reordered; see Table 8-5.
Table 8-5. Different Memory Fence Methods
Method | Operations Before Fence | Operations After Fence |
---|---|---|
void fullFence() | loads and stores | loads and stores |
void acquireFence() | loads | loads and stores |
void releaseFence() | loads and stores | stores |
void loadLoadFence() | loads | loads |
void storeStoreFence() | stores | stores |
Summary
In this chapter, we discussed how to use variable handles to access and manipulate variables. The methods in VarHandle can perform reads, writes, and atomic updates to variables. In the next chapter, we’ll discuss the enhancements to method handles in Java 9.