JEP 193:可变手柄
概括
java.util.concurrent.atomic
定义一个标准方法来调用对象字段和数组元素上的各种and操作的等价物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
(通常会遇到比操作本身更多的开销),或者sun.misc.Unsafe
对 JVM 内部函数使用不安全(且不可移植且不受支持)的API。内在函数速度更快,因此它们被广泛使用,但损害了安全性和可移植性。
如果没有这个 JEP,随着原子 API 扩展以覆盖额外的访问一致性策略(与最近的 C++11 内存模型一致)作为 Java 内存模型修订的一部分,这些问题预计会变得更糟。
描述
变量句柄是对变量的类型化引用,支持多种访问模式下对变量的读写访问。支持的变量类型包括实例字段、静态字段和数组元素。正在考虑并可能支持其他变量类型,例如数组视图、将字节或字符数组视为长数组以及由ByteBuffer
s 描述的堆外区域中的位置。
变量句柄需要库增强、JVM 增强和编译器支持。此外,它还需要对 Java 语言规范和 Java 虚拟机规范进行少量更新。还考虑了次要的语言增强,即增强编译时类型检查并补充现有语法。
由此产生的规范预计能够以自然的方式扩展到其他类似基元的值类型或其他类似数组的类型(如果它们被添加到 Java 中)。然而,这不是用于控制对多个变量的访问和更新的通用事务机制。可以在本 JEP 的过程中探索表达和实现此类结构的替代形式,并且可能是进一步 JEP 的主题。
变量句柄由单个抽象类建模,其中每个变量访问模式都由签名多态java.lang.invoke.VarHandle
方法表示。
该组访问模式代表最小可行集,旨在与 C/C++11 原子兼容,而不依赖于 Java 内存模型的修订更新。如果需要,将添加其他访问模式。某些访问模式可能不适用于某些变量类型,如果是这样,在关联VarHandle
实例上调用时将抛出UnsupportedOperationException
.
访问模式分为以下几类:
-
读访问模式,例如读取具有易失性存储器排序效果的变量;
-
写访问模式,例如更新具有释放内存排序效果的变量;
-
原子更新访问模式,例如对读和写具有易失性内存顺序影响的变量进行比较和设置;
-
数字原子更新访问模式,例如用于写入的具有简单内存顺序效果的获取和添加以及用于读取的获取内存顺序效果。
-
按位原子更新访问模式,例如 get-and-bitwise-and 具有用于写入的释放内存顺序效果和用于读取的普通内存顺序效果。
后三类通常称为读取-修改-写入模式。
访问模式方法的签名多态特性使变量句柄能够仅使用一个抽象类来支持多种变量种类和变量类型。这避免了变量种类和特定于类型的类的爆炸。此外,即使访问模式方法签名被声明为 的变量参数数组Object
,这种签名多态特征也确保不会对原始值参数进行装箱,也不会将参数打包到数组中。这使得 HotSpot 解释器和 C1/C2 编译器在运行时的行为和性能可预测。
创建实例的方法与生成访问等效或相似变量类型的实例的方法VarHandle
位于同一区域。MethodHandle
创建VarHandle
实例 和静态字段变量类型的实例的方法位于java.lang.invoke.MethodHandles.Lookup
相关接收类中查找字段的过程中并由该过程创建。例如,获取接收器类上名为type 的VarHandle
字段的此类查找可以按如下方式执行:i``int``Foo
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
访问字段的 a 的查找将在生成并返回 之前,执行与提供对同一字段的读写访问权限的VarHandle
a 的查找所执行的访问控制检查完全相同的访问控制检查(代表查找类)MethodHandle
(参见类find{,Static}{Getter,Setter}
中的方法MethodHandles.Lookup
)。
UnsupportedOperationException
在以下条件下调用访问模式方法时将 抛出异常:
-
将 a 的访问模式方法写入
VarHandle
最终字段。 -
用于引用变量类型或非数字类型(例如)的基于数字的访问模式方法(
getAndAdd
和) 。addAndGet``boolean
-
用于引用变量类型或
float
和double
类型的基于位的访问模式方法(后一个限制可能会在未来的修订中删除)
无需将字段标记为volatile
关联VarHandle
即可执行易失性访问。实际上,volatile
修饰符(如果存在)将被忽略。这与java.util.concurrent.atomic.Atomic{Int, Long, Reference}FieldUpdater
相应字段必须标记为易失性的行为不同。在已知并不总是需要某些易失性访问的某些情况下,这可能过于严格。
为基于数组的变量类型创建VarHandle
实例的方法位于java.lang.invoke.MethodHandles
(请参阅类arrayElement{Getter, Setter}
中的方法MethodHandles
)。例如,可以按如下方式创建aVarHandle
到 的数组:int
VarHandle intArrayHandle = MethodHandles.arrayElementVarHandle(int[].class);
UnsupportedOperationException
在以下条件下调用访问模式方法时将抛出异常:
-
用于数组组件引用变量类型或非数字类型(例如)的基于数字的访问模式方法(
getAndAdd
和)addAndGet``boolean
-
用于引用变量类型或
float
和double
类型的基于位的访问模式方法(后一个限制可能会在未来的修订中删除)
变量类型的变量类型(实例字段、静态字段和数组元素)支持所有基本类型和引用类型。其他变量类型可能支持这些类型的全部或子集。
为基于数组视图的变量类型创建VarHandle
实例的方法也位于java.lang.invoke.MethodHandles
.例如,可以按如下方式创建将VarHandle
数组视为byte
未对齐数组的数组:long
VarHandle longArrayViewHandle = MethodHandles.byteArrayViewVarHandle(
long[].class, java.nio.ByteOrder.BIG_ENDIAN);
尽管可以使用 实现类似的机制java.nio.ByteBuffer
,但它需要ByteBuffer
创建一个包装byte
数组的实例。由于逃逸分析的脆弱性以及访问必须通过实例ByteBuffer
,这并不总是能保证可靠的性能。在未对齐访问的情况下,除普通访问模式之外的所有方法都将抛出IllegalStateException
。对于对齐访问,某些易失性操作是可能的,具体取决于变量类型。此类VarHandle
实例可用于向量化数组访问。
访问模式方法的参数数量、参数类型和返回类型由变量种类、变量类型和访问模式的特征决定。VarHandle
创建方法(例如前面描述的那些)将记录需求。例如,compareAndSet
先前查找的VH_FOO_FIELD_I
句柄上的 a 需要 3 个参数、一个接收者实例Foo
以及两个int
表示预期值和实际值的 s:
Foo f = ...
boolean r = VH_FOO_FIELD_I.compareAndSet(f, 0, 1);