跳到主要内容

JEP 193:变量句柄

QWen Max 中英对照

概述

定义一个标准方法,用于在对象字段和数组元素上调用各种 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(通常会遇到比操作本身更多的开销),或者使用不安全(且不可移植且不受支持的)sun.misc.Unsafe API 来实现 JVM 内部函数。内部函数速度更快,因此它们已被广泛使用,但牺牲了安全性和可移植性。

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

描述

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

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

如果将来向 Java 中添加更多的类原语值类型或类数组类型,那么生成的规范预计可以以自然的方式进行扩展。然而,这并不是一种用于控制对多个变量访问和更新的通用事务机制。在本 JEP 的过程中可能会探索表达和实现此类结构的替代形式,并且这些替代形式可能会成为进一步 JEP 的主题。

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

这组访问模式代表了一个最小可行集,其设计目的是在不依赖于 Java 内存模型修订更新的情况下,与 C/C++11 原子操作保持兼容。如果需要,将会添加更多的访问模式。某些访问模式可能不适用于特定的变量类型,如果是这样,在关联的 VarHandle 实例上调用时将抛出 UnsupportedOperationException 异常。

访问模式分为以下几类:

  1. 读取访问模式,例如以易变的内存排序效果读取变量;

  2. 写入访问模式,例如以释放内存排序效果更新变量;

  3. 原子更新访问模式,例如对一个变量进行比较并交换的操作,同时对读和写具有易变内存排序效果;

  4. 数值原子更新访问模式,例如获取并加的操作,对写入具有普通内存排序效果,对读取具有获取内存排序效果;

  5. 按位原子更新访问模式,例如获取并按位与的操作,对写入具有释放内存排序效果,对读取具有普通内存排序效果。

后面三种类别通常被称为读-改-写模式。

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

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

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

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 字段的访问模式方法编写写权限。

  • 基于数值的访问模式方法(如 getAndAddaddAndGet),适用于引用变量类型或非数值类型(例如 boolean)。

  • 针对引用变量类型或 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

  • 基于数值的访问模式方法(getAndAddaddAndGet),适用于数组组件引用变量类型或非数值类型(例如 boolean 类型)。

  • 基于位运算的访问模式方法,适用于引用变量类型或 floatdouble 类型(后者的限制可能会在未来的版本中移除)。

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

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

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 类型的值,分别表示期望值和实际值:

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

相比之下,getAndSet 需要两个参数,一个是接收者 Foo 的实例,另一个是将要设置的值 int

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

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

为了在运行时获得可预测的行为和性能,VarHandle 实例应保存在静态最终字段中(如同 Atomic{Int, Long, Reference}FieldUpdater 实例所要求的那样)。这确保了访问模式方法调用时会发生常量折叠,例如折叠掉方法签名检查和/或参数类型转换检查。

注意:未来的 HotSpot 增强功能可能会支持对存储在非静态 final 字段、方法参数或局部变量中的 VarHandleMethodHandle 实例进行常量折叠。

可以使用 MethodHandles.Lookup.findVirtualVarHandle 访问模式方法生成一个 MethodHandle。例如,为特定的变量类型和种类生成一个针对 "compareAndSet" 访问模式的 MethodHandle

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

然后可以调用 MethodHandle,并将一个变量类型和类型兼容的 VarHandle 实例作为第一个参数:

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

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

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

这样的使用 findVirtualMethodHandle 查找将会执行一个 asType 转换来调整参数和返回值。其行为等同于使用 MethodHandles.varHandleInvoker 生成的 MethodHandle,类似于 MethodHandles.invoker

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 字节码行为。预计这种行为可以通过定义从访问模式方法调用到 MethodHandle 的转换来指定,然后使用相同的参数通过 invokeExact 调用该 MethodHandle(参见之前对 MethodHandles.Lookup.findVirtual 的使用)。

重要的是,针对所支持的变量类型、种类和访问模式的 VarHandle 实现必须具备可靠的高效性,并满足性能目标。利用签名多态方法有助于避免装箱和数组打包。实现将:

  • 位于 java.lang.invoke 包中,HotSpot 会将该包中类的 final 字段视为真正的 final,这使得当 VarHandle 本身被引用到 static final 字段时能够进行常量折叠;

  • 利用 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)以减少在例如 fork/join 框架或 HashMapConcurrentHashMap 中的范围检查强度。

VarHandle 的实现应该尽量减少对 java.lang.invoke 包内其他类的依赖,以避免增加启动时间,并防止在静态初始化期间出现循环依赖。例如,ConcurrentHashMap 被这些类所使用,如果 ConcurrentHashMap 被修改为使用 VarHandles,则需要确保不会引入循环依赖。其他更隐蔽的循环依赖可能出现在 ThreadLocalRandom 及其对 AtomicInteger 的使用中。此外,还希望包含 VarHandle 方法调用的方法不会导致 C2 HotSpot 编译时间的过度增加。

内存屏障

围栏操作被定义为 VarHandle 类的静态方法,代表了用于细粒度控制内存排序的最小可行集合。

/**
* Ensures that loads and stores before the fence will not be
* reordered with loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_seq_cst)
*/
public static void fullFence() {}

/**
* Ensures that loads before the fence will not be reordered with
* loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_acquire)
*/
public static void acquireFence() {}

/**
* Ensures that loads and stores before the fence will not be
* reordered with stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_release)
*/
public static void releaseFence() {}

/**
* Ensures that loads before the fence will not be reordered with
* loads after the fence.
*/
public static void loadLoadFence() {}

/**
* Ensures that stores before the fence will not be reordered with
* stores after the fence.
*/
public static void storeStoreFence() {}

完整内存屏障(full fence)在排序保证方面比获取屏障(acquire fence)更强,而获取屏障又比加载加载屏障(load load fence)更强。同样,完整内存屏障比释放屏障(release fence)更强,而释放屏障又比存储存储屏障(store store fence)更强。

可达性围栏

可达性屏障被定义为 java.lang.ref.Reference 上的静态方法:

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

/**
* Ensures that the object referenced by the given reference
* remains <em>strongly reachable</em> (as defined in the {@link
* java.lang.ref} package documentation), regardless of any prior
* actions of the program that might otherwise cause the object to
* become unreachable; thus, the referenced object is not
* reclaimable by garbage collection at least until after the
* invocation of this method. Invocation of this method does not
* itself initiate garbage collection or finalization.
*
* @param ref the reference. If null, this method has no effect.
*/
public static void reachabilityFence(Object ref) {}

}

参见 JDK-8133348

目前,提供一个注解(比如说 @Finalized)来声明在某个方法上,使得在编译时或运行时其效果就如同该方法体被如下方式包装了一样,这超出了范围:

// 示例代码块(仅用于说明)
@Finalized
void exampleMethod() {
// 方法体
}
java

在这个上下文中,“out of scope” 意味着这一功能当前并未被支持或实现。

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

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

替代方案

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

多年来,在这些问题被讨论的过程中,已经提出了几种其他替代方案,包括那些基于字段引用的方案,但都因语法、效率和/或可用性原因被认为不可行而遭到拒绝。

在之前版本的这份 JEP(Java 改进提案)中曾考虑过语法增强,但被认为过于“神奇”,因为 volatile 关键字的重载使用会作用于浮动接口,一个用于引用类型,另一个则针对每种支持的原始类型。

在该 JEP 的早期版本中,曾考虑过从 VarHandle 扩展的泛型类型。然而,考虑到未来 Java 版本将包含值类型和基于原始类型的泛型(参见 JEP 218),以及通过 Arrays 2.0 改进的数组,这种带有增强型多态签名的泛型类型以及对装箱类型变量的特殊处理被认为尚不成熟。

在该 JEP 的早期版本中,还考虑了一种特定于实现的 invokedynamic 方法。这要求使用和不使用 invokedynamic 的编译方法调用在语义上要仔细对齐以保持一致。此外,在诸如 ConcurrentHashMap 等核心类中使用 invokedynamic 将导致循环依赖。

测试

压力测试将使用 jcstress 框架进行开发。

风险与假设

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

依赖

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

此 JEP 并不依赖于 JEP 188: Java 内存模型更新