1年前 (2023-11-13)  Java系列 |   抢沙发  199 
文章评分 0 次,平均分 0.0

Java 9为开发人员带来了许多新的有用功能。

其中之一是java.lang.invoke.VarHandle API—表示变量句柄—我们将在本文中对此进行探讨

什么是变量句柄

通常,变量句柄只是对变量的类型化引用。变量可以是类的数组元素、实例或静态字段。

VarHandle类在特定条件下提供对变量的写和读访问。

VarHandles是不可变的,并且没有可见的状态。更重要的是,它们不能被细分。

每个VarHandle都有:

  • 泛型类型T,它是由该VarHandle表示的每个变量的类型
  • 坐标类型CT的列表,这是坐标表达式的类型,允许定位此VarHandle引用的变量

坐标类型列表可能为空。

VarHandle的目标是定义一个标准,用于在字段和数组元素上调用java.util.concurrent.atomicsun.msic.Unsafe操作的等效项。

这些操作大多是原子操作或有序操作——例如,原子变量的incrementation

创建变量句柄

要使用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;
}

准则和惯例

按照惯例,我们应该将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时,我们可以使用相同的模式。

既然我们知道了这一点,让我们看看我们如何在实践中使用它们。

公共变量的变量句柄

现在,我们可以使用findVarHandle()方法为public TestVariable获取一个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类。

私有变量的变量句柄

如果我们有一个私有成员,并且我们需要一个变量句柄来处理这样的变量,我们可以使用private LookupIn()方法来获得它:

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));

在这里,我们选择了private LookupIn()方法,它比普通的lookup()方法具有更广泛的访问权限。这允许我们访问私有、公共或受保护的变量。

在Java9之前,此操作的等效API是Unsafe类和Reflection API中的setAccessible()方法。

然而,这种方法也有其缺点。例如,它只适用于变量的特定实例。

在这种情况下,VarHandle是一个更好、更快的解决方案。

数组的变量句柄

我们可以使用前面的语法来获取数组字段。

但是,我们也可以获得特定类型的数组的VarHandle:

VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);

assertEquals(2, arrayVarHandle.coordinateTypes().size());
assertEquals(int[].class, arrayVarHandle.coordinateTypes().get(0));

调用VarHandle方法

大多数VarHandle方法都需要数量可变的Object类型的参数。使用Object…作为参数将禁用静态参数检查。

所有的参数检查都是在运行时完成的。此外,不同的方法期望具有不同类型的不同数量的参数。

如果我们不能提供适当数量的具有适当类型的参数,方法调用将抛出WrongMethodTypeException

例如,get()至少需要一个参数,这有助于定位变量,但set()需要一个以上的参数,即要分配给变量的值。

变量句柄的访问模式

通常,VarHandle类的所有方法都属于五种不同的访问模式。

让我们在下一小节中逐一介绍它们。

读取访问

具有读取访问级别的方法允许在指定的内存排序效果下获得变量的值。这种访问模式有几种方法,如:get()、getAcquire()、getVolatile()getOpaque()

我们可以很容易地在VarHandle上使用get()方法:

assertEquals(1, (int) PUBLIC_TEST_VARIABLE.get(this));

get()方法只将CoordinateTypes作为参数,因此我们可以在本例中简单地使用它。

写入访问

具有写入访问级别的方法允许我们在特定的内存排序效果下设置变量的值。

与具有读取访问权限的方法类似,我们有几种具有写入访问权限的方式:set()、setOpaque()、setVolatile()setRelease()

我们可以在VarHandle上使用set()方法:

VARIABLE_TO_SET.set(this, 15);
assertEquals(15, (int) VARIABLE_TO_SET.get(this));

set()方法需要至少两个参数。第一个将帮助定位变量,而第二个是要设置给变量的值。

原子更新访问

具有此访问级别的方法可以用于原子更新变量的值。

让我们使用compareAndSet()方法来查看效果:

VARIABLE_TO_COMPARE_AND_SET.compareAndSet(this, 1, 100);
assertEquals(100, (int) VARIABLE_TO_COMPARE_AND_SET.get(this));

除了CoordinateTypes之外,compareAndSet()方法还有两个附加值:oldValuenewValue。如果变量的值等于oldVariable,则该方法设置该值,否则保持不变。

数值的原子更新访问

这些方法允许在特定的内存排序效果下执行诸如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()方法首先返回变量的值,然后添加提供的值。

Bitwise原子更新访问

具有此访问权限的方法允许我们在特定的内存排序效果下原子地执行逐位操作。

让我们看一个使用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()方法,就会发生这种情况。

内存排序效果

我们之前提到,VarHandle方法允许在特定的内存排序效果下访问变量。

对于大多数方法,有4种内存排序效果:

  • 纯读写保证了32位以下引用和基元的逐位原子性。此外,它们对其他特征没有施加排序约束。
  • 不透明操作是按位原子的,并且相对于对同一变量的访问是一致有序的。
  • 获取和释放操作遵循不透明特性。此外,只有在匹配Release模式写入之后,才会对Acquire读取进行排序。
  • 易失性操作彼此之间是完全有序的。

非常重要的是要记住,访问模式将覆盖以前的内存排序效果。这意味着,例如,如果我们使用get(),它将是一个纯读取操作,即使我们将变量声明为volatile

因此,开发人员在使用VarHandle操作时必须格外小心。

结论

在本文中,我们介绍了变量句柄以及如何使用它们。

这个主题相当复杂,因为变量句柄旨在允许低级别的操作,除非必要,否则不应该使用它们。

代码链接:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-9-new-features

原文地址:https://www.baeldung.com/java-variable-handles

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2925.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册