Skip to content

JEP 193: Variable Handles | 变量句柄

摘要

定义一种标准的方式来调用各种 java.util.concurrent.atomicsun.misc.Unsafe 操作,用于对象字段和数组元素,提供一组标准的栅栏操作,以实现对内存顺序的细粒度控制,并提供一种标准的可达性栅栏操作,以确保引用对象保持强引用。

目标

需要实现以下目标:

  • 安全性。不允许将 Java 虚拟机置于损坏的内存状态。例如,只能使用可转换为字段类型的实例来更新对象的字段,只有在数组索引在数组边界内时才能访问数组元素。
  • 完整性。访问对象的字段遵循与 getfieldputfield 字节码相同的访问规则,并额外限制了不能更新对象的 final 字段。(注意:这些安全性和完整性规则也适用于提供对字段进行读取或写入访问的 MethodHandles 。)
  • 性能。性能特性必须与等效的 sun.misc.Unsafe 操作相同或相似(具体而言,生成的汇编代码应该几乎相同,除了无法消除的某些安全检查)。
  • 可用性。API 必须优于 sun.misc.Unsafe API 。

希望 API 能达到与 java.util.concurrent.atomic API 相当或更好的水平,但这不是必需的。

动机

随着 Java 中并发和并行编程的不断扩展,程序员越来越不满意不能使用 Java 的构造来对单个类的字段进行原子或有序操作;例如,原子地递增一个 count 字段。到目前为止,实现这些效果的唯一方式是使用独立的 AtomicInteger (增加了空间开销和额外的并发问题以管理间接引用),或者在某些情况下使用原子 FieldUpdater (往往比操作本身还要增加开销),或者使用不安全(不可移植和不受支持)的 JVM 内嵌 sun.misc.Unsafe API。内嵌函数更快,因此它们已被广泛使用,但这对安全性和可移植性产生了不利影响。

如果没有这个 JEP,随着原子 API 扩展以覆盖额外的访问一致性策略(与最近的 C++ 11 内存模型一致)作为 Java 内存模型修订的一部分,这些问题预计会变得更糟。

描述

变量句柄是对变量的类型化引用,支持以各种访问模式对变量进行读写访问。支持的变量类型包括实例字段、静态字段和数组元素。正在考虑并可能支持其他变量类型,例如数组视图,将字节数组或字符数组视为长整型数组,以及由 ByteBuffer 描述的非堆区域中的位置。

变量句柄需要对库进行增强,对 JVM 进行增强,并需要编译器支持。此外,还需要对 Java 语言规范和 Java 虚拟机规范进行轻微更新。还考虑了一些轻微的语言增强,以增强编译时类型检查,并补充现有的语法。

预计生成的规范能够以自然的方式扩展到其他类似原始类型或类似数组的类型,如果它们被添加到 Java 中。然而,这并不是一种用于控制对多个变量的访问和更新的通用事务机制。在此 JEP 的过程中可以探索用于表达和实现此类结构的其他形式,并可能成为进一步 JEP 的主题。

变量句柄由一个抽象类 java.lang.invoke.VarHandle 来建模,其中每种变量访问模式都由一个签名多态方法表示。

访问模式集合表示了一个最小可行集,并旨在与 C/C++ 11 原子操作兼容,而不依赖于对 Java 内存模型的修订。如果需要,将添加其他访问模式。对于某些变量类型,某些访问模式可能不适用,如果是这样,当在关联的 VarHandle 实例上调用时,将抛出 UnsupportedOperationException

访问模式分为以下几类:

  1. 读取访问模式,例如使用有序的 volatile 内存顺序效果读取变量;
  2. 写入访问模式,例如使用释放内存顺序效果更新变量;
  3. 原子更新访问模式,例如在具有读写双方有序的 volatile 内存顺序效果的变量上进行比较和设置;
  4. 数值原子更新访问模式,例如使用普通内存顺序效果写入和获取的 get-and-add 操作;
  5. 位操作原子更新访问模式,例如使用释放内存顺序效果写入和普通内存顺序效果读取的 get-and-bitwise-and 操作。

后三类通常称为读取 - 修改 - 写入模式。

访问模式方法的签名多态特性使得变量句柄能够使用同一个抽象类支持多种变量类型和变量种类,避免了变量种类和类型特定类的爆炸性增长。此外,尽管访问模式方法的签名声明为 Object 类型的可变参数数组,但签名多态特性确保不会将基本值参数装箱,也不会将参数打包到数组中。这使得 HotSpot 解释器和 C1/C2 编译器在运行时能够获得可预测的行为和性能。

创建 VarHandle 实例的方法位于与用于生成访问等效或类似变量种类的 MethodHandle 实例的方法相同的区域。

用于创建实例和静态字段变量种类的 VarHandle 实例的方法位于 java.lang.invoke.MethodHandles.Lookup 中,并通过在关联的接收类中查找字段的过程来创建。例如,获取接收类 Foo 上名为 i、类型为 int 的字段的 VarHandle ,可以按以下方式执行查找:

java
class Foo {
    int i;

    ...
}

...

class Bar {
    static final VarHandle VH_FOO_FIELD_I;

    static {
        try {
            VH_FOO_FIELD_I = MethodHandles.lookup().
                in(Foo.class).
                findVarHandle(Foo.class, "i", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

在生成并返回 VarHandle 之前,访问字段的 VarHandle 查找将执行与查找给予该字段读取和写入访问权限的 MethodHandle 执行的完全相同的访问控制检查(代表查找类)。请参考 MethodHandles.Lookup 类中的 find{,Static}{Getter,Setter} 方法。

在以下情况下,访问模式方法在被调用时会抛出 UnsupportedOperationException

  • 对于 VarHandle 的写入访问模式方法,如果目标字段是 final 字段。
  • 对于引用变量类型或非数值类型(如 boolean)的数值型访问模式方法(getAndAddaddAndGet)。
  • 对于引用变量类型或 floatdouble 类型的位操作访问模式方法(后者的限制可能在未来的修订中被移除)。

用于访问字段的 VarHandle 不需要将字段标记为 volatile 即可执行 volatile 访问。实际上,如果有 volatile 修饰符存在,它将被忽略。这与 java.util.concurrent.atomic.Atomic{Int, Long, Reference}FieldUpdater 的行为不同,后者要求相应的字段必须标记为 volatile 。在某些情况下,这可能过于限制性,因为已知某些 volatile 访问并不总是必需的。

用于创建基于数组的变量类型的 VarHandle 实例的方法位于 java.lang.invoke.MethodHandles 中(请参考 MethodHandles 类中的 arrayElement{Getter, Setter} 方法)。例如,可以按以下方式创建对 int 数组的 VarHandle

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

在以下情况下,访问模式方法在被调用时会抛出 UnsupportedOperationException

  • 对于数组组件引用变量类型或非数值类型(如 boolean)的数值型访问模式方法(getAndAddaddAndGet)。
  • 对于引用变量类型或 floatdouble 类型的位操作访问模式方法(后者的限制可能在未来的修订中被移除)。

所有原始类型和引用类型都支持用于实例字段、静态字段和数组元素的变量种类的变量类型。其他变量种类可能支持所有或其中一部分类型。

用于创建基于数组视图的变量类型的 VarHandle 实例的方法也位于 java.lang.invoke.MethodHandles 中。例如,可以按以下方式创建将 byte 数组视为不对齐的 long 数组的 VarHandle

java
VarHandle longArrayViewHandle = MethodHandles.byteArrayViewVarHandle(
        long[].class, java.nio.ByteOrder.BIG_ENDIAN);

尽管可以使用 java.nio.ByteBuffer 实现类似的机制,但它需要创建一个包装 byte 数组的 ByteBuffer 实例。由于逃逸分析的脆弱性和访问必须通过 ByteBuffer 实例进行,这并不能始终保证可靠的性能。在不对齐访问的情况下,除了普通访问模式方法之外,所有其他访问模式方法都会抛出 IllegalStateException 。此类 VarHandle 实例可用于向量化数组访问。

访问模式方法的参数数量、参数类型和返回类型受变量种类、变量类型和访问模式的特性的控制。VarHandle 创建方法(例如前面描述的方法)将记录要求。例如,对先前查找的 VH_FOO_FIELD_I 句柄执行 compareAndSet 操作需要 3 个参数:接收者 Foo 的实例和两个 int 值,分别为期望值和实际值:

java
Foo f = ...
boolean r = VH_FOO_FIELD_I.compareAndSet(f, 0, 1);

相反,对于 getAndSet 操作,需要 2 个参数:接收者 Foo 的实例和一个 int 值,该值将被设置:

java
int o = (int) VH_FOO_FIELD_I.getAndSet(f, 2);

访问数组元素将需要一个额外的 int 类型的参数,位于接收者和值参数之间(如果有的话),该参数对应于要操作的元素的数组索引。

为了在运行时实现可预测的行为和性能,VarHandle 实例应该保存在静态 final 字段中(与 Atomic{Int, Long, Reference}FieldUpdater 实例的要求相同)。这确保访问模式方法调用的常量折叠,例如折叠方法签名检查和/或参数转换检查。

注意:未来的 HotSpot 改进可能支持在非静态 final 字段、方法参数或局部变量中保存的 VarHandleMethodHandle 的常量折叠和运行时支持。

可以使用 MethodHandles.Lookup.findVirtualVarHandle 访问模式方法生成 MethodHandle 。例如,要为特定的变量种类和类型生成到 “compareAndSet” 访问模式的 MethodHandle

java
Foo f = ...
MethodHandle mhToVhCompareAndSet = MethodHandles.publicLookup().findVirtual(
        VarHandle.class,
        "compareAndSet",
        MethodType.methodType(boolean.class, Foo.class, int.class, int.class));

然后,可以将 MethodHandle 与兼容的 VarHandle 实例绑定作为第一个参数进行调用:

java
boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);

或者可以将 mhToVhCompareAndSet 绑定到 VarHandle 实例,然后进行调用:

java
MethodHandle mhToBoundVhCompareAndSet = mhToVhCompareAndSet
        .bindTo(VH_FOO_FIELD_I);
boolean r = (boolean) mhToBoundVhCompareAndSet.invokeExact(f, 0, 1);

使用 findVirtual 进行的此类 MethodHandle 查找将执行 asType 转换,以调整参数和返回值。这个行为相当于使用 MethodHandles.varHandleInvoker 产生的 MethodHandle,它是 MethodHandles.invoker 的类比:

java
MethodHandle mhToVhCompareAndSet = MethodHandles.varHandleExactInvoker(
        VarHandle.AccessMode.COMPARE_AND_SET,
        MethodType.methodType(boolean.class, Foo.class, int.class, int.class));

boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);

因此,通过包装类可以在擦除或反射场景中使用 VarHandle ,例如替换 java.util.concurrent.Atomic*FieldUpdater/Atomic*Array 类中的 Unsafe 用法。(尽管还需要进一步的工作,以便允许更新者访问声明类中的查找字段。)

访问模式方法调用的源代码编译将遵循对 MethodHandle.invokeExactMethodHandle.invoke 进行签名多态方法调用的相同规则。需要对 Java 语言规范进行以下补充:

  1. VarHandle 类中引用签名多态访问模式方法。
  2. 允许签名多态方法返回除 Object 以外的类型,指示返回类型不是多态的(否则将在调用点处通过强制类型转换声明)。这样可以更容易地调用返回 void 的写入访问方法以及返回 boolean 值的 compareAndSet 方法。

希望对签名多态方法调用的源代码编译进行增强,以执行目标类型化的多态返回类型,从而不需要显式强制类型转换。

注意:希望能够通过方法引用的语法来查找 MethodHandleVarHandle 的语法和运行时支持,例如 VarHandle VH_FOO_FIELD_I = Foo::i ,但这不在此 JEP 的范围内。

访问模式方法调用的运行时调用将遵循与对 MethodHandle.invokeExactMethodHandle.invoke 进行签名多态方法调用的相似规则。需要对 Java 虚拟机规范进行以下补充:

  1. VarHandle 类中引用签名多态访问模式方法。
  2. 指定调用访问模式签名多态方法的 invokevirtual 字节码行为。预计可以通过定义将访问模式方法调用转换为使用相同参数进行 invokeExactMethodHandle 来指定此类行为(请参见先前对 MethodHandles.Lookup.findVirtual 的使用)。

对于支持的变量种类、类型和访问模式的 VarHandle 实现来说,可靠高效并符合性能目标非常重要。利用签名多态方法有助于避免装箱和数组打包。实现将:

  • 位于 java.lang.invoke 包中,其中 HotSpot 将该包中的类的 final 字段视为真正的 final 字段,这样当在静态 final 字段中引用 VarHandle 本身时,就可以进行常量折叠;
  • 利用 JDK 内部注解 @Stable 用于只更改一次的常量折叠,利用 @ForceInline 确保方法即使达到了正常内联阈值也会被内联;以及
  • 使用 sun.misc.Unsafe 进行底层增强的 volatile 访问。

一些 HotSpot 内置函数是必要的,以下是其中的一些列举:

  • 用于 Class.cast 的内置函数,已经添加(参见 JDK-8054492)。在添加此内置函数之前,常量折叠的 Class.cast 将留下可能导致不必要的取消优化的冗余检查。
  • 用于 acquire-get 访问模式的内置函数,可以与 set-release 访问模式的内置函数(参见 sun.misc.Unsafe.putOrdered{Int, Long, Object} )同步访问变量时进行同步。
  • 数组边界检查的内置函数 JDK-8042997 。可以在 java.util.Arrays 中添加静态方法,执行此类检查,并接受一个函数,该函数在检查失败时被调用以返回要抛出的异常或包含在要抛出的异常中的字符串消息。这样的内置函数可以使用无符号值进行更好的比较(因为数组长度始终为正),并更好地将范围检查提升到数组元素的展开循环之外。

此外,HotSpot 还实现了对范围检查的进一步改进( JDK-8073480 )或所需的进一步改进( JDK-8003585 ,以便在分支/合并框架、HashMapConcurrentHashMap 等中对范围检查进行强化优化)。

VarHandle 的实现应尽量减少对 java.lang.invoke 包中其他类的依赖,以避免增加启动时间并避免在静态初始化期间出现循环依赖。例如,这些类使用了 ConcurrentHashMap ,如果将 ConcurrentHashMap 修改为使用 VarHandles ,则需要确保不会引入循环依赖。还可以通过使用 ThreadLocalRandom 及其对 AtomicInteger 的使用来引入其他更微妙的循环。此外,希望不会因为包含 VarHandle 方法调用的方法而不合理地增加 C2 HotSpot 的编译时间。

内存栅栏

栅栏操作是定义在 VarHandle 类上的静态方法,用于实现对内存顺序的细粒度控制。

java
/**
 * 确保栅栏之前的加载和存储不会与栅栏之后的加载和存储重新排序。
 * @apiNote 忽略与 C 和 C++ 的许多语义差异,此方法具有与 atomic_thread_fence(memory_order_seq_cst) 兼容的内存顺序效果
 */
public static void fullFence() {}

/**
 * 确保栅栏之前的加载不会与栅栏之后的加载和存储重新排序。
 * @apiNote 忽略与 C 和 C++ 的许多语义差异,此方法具有与 atomic_thread_fence(memory_order_acquire) 兼容的内存顺序效果
 */
public static void acquireFence() {}

/**
 * 确保栅栏之前的加载和存储不会与栅栏之后的存储重新排序。
 * @apiNote 忽略与 C 和 C++ 的许多语义差异,此方法具有与 atomic_thread_fence(memory_order_release) 兼容的内存顺序效果
 */
public static void releaseFence() {}

/**
 * 确保栅栏之前的加载不会与栅栏之后的加载重新排序。
 */
public static void loadLoadFence() {}

/**
 * 确保栅栏之前的存储不会与栅栏之后的存储重新排序。
 */
public static void storeStoreFence() {}

完整的栅栏比获取栅栏更强(在顺序保证方面),获取栅栏比加载 - 加载栅栏更强。同样,完整的栅栏比释放栅栏更强,释放栅栏比存储 - 存储栅栏更强。

可达性栅栏

可达性栅栏被定义为 java.lang.ref.Reference 类的一个静态方法:

java
class java.lang.ref.Reference {
   // add:

   /**
    * 确保给定引用引用的对象保持<strong>强引用</strong>(如 {@link java.lang.ref} 包文档中所定义),
    * 无论程序中的任何先前操作是否可能导致对象变得不可达;
    * 因此,被引用的对象在此方法调用之后至少在垃圾收集之前不会被回收。
    * 此方法的调用本身不会启动垃圾收集或终结操作。
    *
    * @param ref the reference. If null, this method has no effect.
    */
   public static void reachabilityFence(Object ref) {}

}

参见 JDK-8133348

目前不在范围内的是在方法上声明一个注解,比如 @Finalized ,在编译时或运行时,结果就好像方法体被包装如下:

java
try {
    <method body>
} finally {
    Reference.reachabilityFence(this);
}

预计这样的功能可以通过编译时注解处理器来支持。

备选方案

考虑引入支持 volatile 操作的新型“值类型”。然而,这将与其他类型的属性不一致,并且还需要程序员付出更多的努力。还考虑了依赖于 java.util.concurrent.atomic 中的 FieldUpdater ,但它们的动态开销和使用限制使它们不适合。

在多年的讨论中,还提出了几种其他的替代方案,包括基于字段引用的方案,但由于语法、效率和/或可用性等方面的问题,这些方案都被认为不可行。

在此 JEP 的早期版本中考虑了语法增强,但被认为过于“神奇”,volatile 关键字的重载使用范围只限于浮动接口,一个用于引用,一个用于每种支持的原始类型。

在此 JEP 的早期版本中考虑了扩展自 VarHandle 的泛型类型,但是这样的添加,具有增强的泛型类型的多态签名和对装箱类型变量的特殊处理,被认为在未来具有值类型和基本类型泛型的 Java 版本(使用 JEP 218)和改进的数组(使用 Arrays 2.0 )中还不成熟。

在此 JEP 的早期版本中还考虑了基于特定实现的 invokedynamic 方法。这要求编译时具有和不具有 invokedynamic 的方法调用在语义上保持一致。此外,使用 invokedynamic 在核心类(例如 ConcurrentHashMap )中会导致循环依赖。

测试

将使用 jcstress 测试工具开发压力测试。

风险和假设

已经针对 VarHandle 进行了原型实现,并通过纳秒级别的基准测试和 fork/join 基准测试进行了性能测试,其中 fork/join 库中对 sun.misc.Unsafe 的使用已被 VarHandle 替换。到目前为止,没有观察到重大的性能问题,并且 HotSpot 编译器问题似乎并不困难(折叠类型检查和改进数组边界检查)。因此,我们对此方法的可行性感到有信心。然而,我们预计需要进行更多的实验,以确保编译技术在这些构造最常使用的性能关键环境中的可靠性。

依赖

java.util.concurrent 中的类(以及 JDK 中标识的其他区域)将从 sun.misc.Unsafe 迁移到 VarHandle

此 JEP 不依赖于 JEP 188: Java Memory Model Update