© Fu Cheng 2018

Fu Cheng, Exploring Java 9, https://doi.org/10.1007/978-1-4842-3330-6_8

8. Variable Handles

Fu Cheng

(1)Auckland, New Zealand

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.67.27