Java 9 变量句柄揭秘-Java快速进阶教程
1. 简介
Java 9为开发人员带来了许多新的有用功能。
其中之一是java.lang.invoke.VarHandleAPI - 表示变量句柄 - 我们将在本文中探讨。
2. 什么是可变手柄?
通常,变量句柄只是对变量的类型化引用。变量可以是类的数组元素、实例或静态字段。
类在特定条件下提供对变量的写入和读取访问。
VarHandle是不可变的,没有可见状态。更重要的是,它们不能被子类化。
每个VarHandle都有:
- 泛型类型T,它是此VarHandle 表示的每个变量的类型
- 坐标类型CT 的列表,CT 是坐标表达式的类型,允许定位此VarHandle 引用的变量
坐标类型的列表可能为空。
VarHandle的目标是定义一个标准,用于调用字段和数组元素上的 java.util.concurrent.atomic和sun.misc.Unsafe操作的等效项。
这些操作大多是原子或有序操作,例如原子场增量。
3. 创建变量句柄
要使用VarHandle,我们首先需要有变量。
让我们声明一个简单的类,其中包含我们将在示例中使用的不同int类型的变量:
public class VariableHandlesUnitTest {
public int publicTestVariable = 1;
private int privateTestVariable = 1;
public int variableToSet = 1;
public int variableToCompareAndSet = 1;
public int variableToGetAndAdd = 0;
public byte variableToBitwiseOr = 0;
}
3.1. 准则和惯例
作为惯例,我们应该将VarHandles 声明为静态最终字段,并在静态块中显式初始化它们。此外,我们通常使用相应字段名称的大写版本作为其名称。
例如,以下是Java本身如何在内部使用VarHandles来实现AtomicReference:
private volatile V value;
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
大多数时候,我们在使用VarHandles 时可以使用相同的模式。
现在我们知道了这一点,让我们继续前进,看看如何在实践中使用它们。
3.2. 公共变量的变量句柄
现在我们可以使用findVarHandle() 方法获取publicTestVariable的VarHandle:
VarHandle PUBLIC_TEST_VARIABLE = MethodHandles
.lookup()
.in(VariableHandlesUnitTest.class)
.findVarHandle(VariableHandlesUnitTest.class, "publicTestVariable", int.class);
assertEquals(1, PUBLIC_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PUBLIC_TEST_VARIABLE.coordinateTypes().get(0));
我们可以看到这个VarHandle的coordinateTypes属性不是空的,并且有一个元素,那就是我们的VariableHandlesUnitTest类。
3.3. 私有变量的变量句柄
如果我们有一个私有成员,并且我们需要一个变量的变量句柄,我们可以使用privateLookupIn() 方法获取它:
VarHandle PRIVATE_TEST_VARIABLE = MethodHandles
.privateLookupIn(VariableHandlesUnitTest.class, MethodHandles.lookup())
.findVarHandle(VariableHandlesUnitTest.class, "privateTestVariable", int.class);
assertEquals(1, PRIVATE_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PRIVATE_TEST_VARIABLE.coordinateTypes().get(0));
在这里,我们选择了privateLookupIn()方法,它比普通的lookup()具有更广泛的访问权限。这使我们能够访问私有、公共或受保护的变量。
在Java 9之前,此操作的等效API是Unsafe类和来自ReflectionAPI的setAccessible()方法。
但是,这种方法有其缺点。例如,它仅适用于变量的特定实例。
在这种情况下,VarHandle是一种更好、更快的解决方案。
3.4. 数组的变量句柄
我们可以使用前面的语法来获取数组字段。
但是,我们也可以获取特定类型的数组的VarHandle:
VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);
assertEquals(2, arrayVarHandle.coordinateTypes().size());
assertEquals(int[].class, arrayVarHandle.coordinateTypes().get(0));
我们现在可以看到,这样的VarHandle有两种坐标类型 int 和 [],它们表示一个int原语数组。
4. 调用变量句柄方法
大多数VarHandle方法都需要可变数量的 Object 类型的参数。使用对象...作为参数禁用静态参数检查。
所有参数检查都在运行时完成。此外,不同的方法期望具有不同数量的不同类型的参数。
如果我们未能提供具有正确类型的适当数量的参数,则方法调用将抛出WrongMethodTypeException。
例如,get() 需要至少一个参数,这有助于定位变量,但set() 需要更多参数,这是要分配给变量的值。
5. 变量句柄访问模式
通常,VarHandle类的所有方法都分为五种不同的访问模式。
让我们在接下来的小节中逐一介绍它们。
5.1. 读取权限
具有读取访问级别的方法允许在指定的内存排序效果下获取变量的值。这种访问模式有几种方法,例如:get(),getAcquire(),getVolatile()和getOpaque()。
我们可以轻松地在VarHandle 上使用get() 方法:
assertEquals(1, (int) PUBLIC_TEST_VARIABLE.get(this));
get() 方法只接受CoordinateType作为参数,因此我们可以简单地在本例中使用它。
5.2. 写入权限
具有写入访问级别的方法允许我们在特定的内存排序效果下设置变量的值。
与具有读取访问权限的方法类似,我们有几个具有写入权限的方法:set(),setOpaque(),setVolatile()和setRelease()。
我们可以在VarHandle 上使用set() 方法:
VARIABLE_TO_SET.set(this, 15);
assertEquals(15, (int) VARIABLE_TO_SET.get(this));
set() 方法至少需要两个参数。第一个将帮助定位变量,而第二个是要设置为变量的值。
5.3. 原子更新访问
具有此访问级别的方法可用于以原子方式更新变量的值。
让我们使用compareAndSet() 方法查看效果:
VARIABLE_TO_COMPARE_AND_SET.compareAndSet(this, 1, 100);
assertEquals(100, (int) VARIABLE_TO_COMPARE_AND_SET.get(this));
除了CoordinateTypes,compareAndSet() 方法还需要两个额外的值:oldValue和newValue。该方法设置变量的值(如果它等于oldVariable),否则保持不变。
5.4. 数字原子更新访问
这些方法允许在特定内存排序效果下执行数字运算,例如getAndAdd()。
让我们看看如何使用VarHandle 执行原子操作:
int before = (int) VARIABLE_TO_GET_AND_ADD.getAndAdd(this, 200);
assertEquals(0, before);
assertEquals(200, (int) VARIABLE_TO_GET_AND_ADD.get(this));
在这里,getAndAdd() 方法首先返回变量的值,然后添加提供的值。
5.5. 按位原子更新访问
具有此访问权限的方法允许我们在特定的内存排序效果下原子地执行按位运算。
让我们看一个使用getAndBitwiseOr() 方法的示例:
byte before = (byte) VARIABLE_TO_BITWISE_OR.getAndBitwiseOr(this, (byte) 127);
assertEquals(0, before);
assertEquals(127, (byte) VARIABLE_TO_BITWISE_OR.get(this));
此方法将获取变量的值并对其执行按位 OR 运算。
如果方法调用未能将方法所需的访问模式与变量允许的访问模式匹配,则该方法调用将引发IllegalAccessException。
例如,如果我们尝试在最终变量上使用set() 方法,就会发生这种情况。
6. 记忆排序效果
我们之前提到过,VarHandle方法允许在特定内存排序效果下访问变量。
对于大多数方法,有 4 种内存排序效果:
- 普通读写保证了 32 位以下引用和基元的按位原子性。此外,它们对其他特征没有顺序约束。
- 不透明操作是按位原子的,并且相对于对同一变量的访问进行连贯排序。
- 获取和发布操作遵循不透明属性。此外,只有在匹配发布模式写入后,才会对获取读取进行排序。
- 易失性操作彼此之间是完全有序的。
请务必记住,访问模式将覆盖以前的内存排序效果。这意味着,例如,如果我们使用get(),即使我们将变量声明为易失性,它也将是一个普通读取操作。
因此,开发人员在使用VarHandle操作时必须格外小心。