JEP 193: Variable Handles | 变量句柄
摘要
定义一种标准的方式来调用各种 java.util.concurrent.atomic
和 sun.misc.Unsafe
操作,用于对象字段和数组元素,提供一组标准的栅栏操作,以实现对内存顺序的细粒度控制,并提供一种标准的可达性栅栏操作,以确保引用对象保持强引用。
目标
需要实现以下目标:
- 安全性。不允许将 Java 虚拟机置于损坏的内存状态。例如,只能使用可转换为字段类型的实例来更新对象的字段,只有在数组索引在数组边界内时才能访问数组元素。
- 完整性。访问对象的字段遵循与
getfield
和putfield
字节码相同的访问规则,并额外限制了不能更新对象的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
。
访问模式分为以下几类:
- 读取访问模式,例如使用有序的
volatile
内存顺序效果读取变量; - 写入访问模式,例如使用释放内存顺序效果更新变量;
- 原子更新访问模式,例如在具有读写双方有序的
volatile
内存顺序效果的变量上进行比较和设置; - 数值原子更新访问模式,例如使用普通内存顺序效果写入和获取的 get-and-add 操作;
- 位操作原子更新访问模式,例如使用释放内存顺序效果写入和普通内存顺序效果读取的 get-and-bitwise-and 操作。
后三类通常称为读取 - 修改 - 写入模式。
访问模式方法的签名多态特性使得变量句柄能够使用同一个抽象类支持多种变量类型和变量种类,避免了变量种类和类型特定类的爆炸性增长。此外,尽管访问模式方法的签名声明为 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
字段。 - 对于引用变量类型或非数值类型(如
boolean
)的数值型访问模式方法(getAndAdd
和addAndGet
)。 - 对于引用变量类型或
float
和double
类型的位操作访问模式方法(后者的限制可能在未来的修订中被移除)。
用于访问字段的 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
:
VarHandle intArrayHandle = MethodHandles.arrayElementVarHandle(int[].class);
在以下情况下,访问模式方法在被调用时会抛出 UnsupportedOperationException
:
- 对于数组组件引用变量类型或非数值类型(如
boolean
)的数值型访问模式方法(getAndAdd
和addAndGet
)。 - 对于引用变量类型或
float
和double
类型的位操作访问模式方法(后者的限制可能在未来的修订中被移除)。
所有原始类型和引用类型都支持用于实例字段、静态字段和数组元素的变量种类的变量类型。其他变量种类可能支持所有或其中一部分类型。
用于创建基于数组视图的变量类型的 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
值,分别为期望值和实际值:
Foo f = ...
boolean r = VH_FOO_FIELD_I.compareAndSet(f, 0, 1);
相反,对于 getAndSet
操作,需要 2 个参数:接收者 Foo
的实例和一个 int
值,该值将被设置:
int o = (int) VH_FOO_FIELD_I.getAndSet(f, 2);
访问数组元素将需要一个额外的 int
类型的参数,位于接收者和值参数之间(如果有的话),该参数对应于要操作的元素的数组索引。
为了在运行时实现可预测的行为和性能,VarHandle
实例应该保存在静态 final
字段中(与 Atomic{Int, Long, Reference}FieldUpdater
实例的要求相同)。这确保访问模式方法调用的常量折叠,例如折叠方法签名检查和/或参数转换检查。
注意:未来的 HotSpot 改进可能支持在非静态
final
字段、方法参数或局部变量中保存的VarHandle
或MethodHandle
的常量折叠和运行时支持。
可以使用 MethodHandles.Lookup.findVirtual
为 VarHandle
访问模式方法生成 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);
使用 findVirtual
进行的此类 MethodHandle
查找将执行 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.invokeExact
和 MethodHandle.invoke
进行签名多态方法调用的相同规则。需要对 Java 语言规范进行以下补充:
- 在
VarHandle
类中引用签名多态访问模式方法。 - 允许签名多态方法返回除
Object
以外的类型,指示返回类型不是多态的(否则将在调用点处通过强制类型转换声明)。这样可以更容易地调用返回void
的写入访问方法以及返回boolean
值的compareAndSet
方法。
希望对签名多态方法调用的源代码编译进行增强,以执行目标类型化的多态返回类型,从而不需要显式强制类型转换。
注意:希望能够通过方法引用的语法来查找
MethodHandle
或VarHandle
的语法和运行时支持,例如VarHandle VH_FOO_FIELD_I = Foo::i
,但这不在此 JEP 的范围内。
访问模式方法调用的运行时调用将遵循与对 MethodHandle.invokeExact
和 MethodHandle.invoke
进行签名多态方法调用的相似规则。需要对 Java 虚拟机规范进行以下补充:
- 在
VarHandle
类中引用签名多态访问模式方法。 - 指定调用访问模式签名多态方法的
invokevirtual
字节码行为。预计可以通过定义将访问模式方法调用转换为使用相同参数进行invokeExact
的MethodHandle
来指定此类行为(请参见先前对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 ,以便在分支/合并框架、HashMap
或 ConcurrentHashMap
等中对范围检查进行强化优化)。
VarHandle
的实现应尽量减少对 java.lang.invoke
包中其他类的依赖,以避免增加启动时间并避免在静态初始化期间出现循环依赖。例如,这些类使用了 ConcurrentHashMap
,如果将 ConcurrentHashMap
修改为使用 VarHandles
,则需要确保不会引入循环依赖。还可以通过使用 ThreadLocalRandom
及其对 AtomicInteger
的使用来引入其他更微妙的循环。此外,希望不会因为包含 VarHandle
方法调用的方法而不合理地增加 C2 HotSpot 的编译时间。
内存栅栏
栅栏操作是定义在 VarHandle
类上的静态方法,用于实现对内存顺序的细粒度控制。
/**
* 确保栅栏之前的加载和存储不会与栅栏之后的加载和存储重新排序。
* @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
类的一个静态方法:
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
,在编译时或运行时,结果就好像方法体被包装如下:
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 。