跳到主要内容

JEP 193:可变手柄

概括

java.util.concurrent.atomic定义一个标准方法来调用对象字段和数组元素上的各种and操作的等价物sun.misc.Unsafe,一组用于细粒度控制内存排序的标准栅栏操作,以及一个标准的可达性栅栏操作以确保引用的对象保持强可达性。

目标

以下是所需的目标:

  • 安全。一定不能将 Java 虚拟机置于损坏的内存状态。例如,对象的字段只能使用可转换为字段类型的实例进行更新,或者如果数组索引位于数组边界内,则只能在数组内访问数组元素。

  • 正直。除了不能更新对象字段的约束之外,对对象字段的访问遵循getfield与字节码相同的访问规则。 (注意:此类安全性和完整性规则也适用于授予对字段的读或写访问权限。)putfield``final``MethodHandles

  • 表现。性能特征必须与等效操作相同或相似sun.misc.Unsafe(具体来说,生成的汇编代码应该与某些不能折叠的安全检查模数几乎相同)。

  • 可用性。 API 必须比sun.misc.UnsafeAPI 更好。

API 与java.util.concurrent.atomicAPI 一样好是可取的,但不是必需的。

动机

随着 Java 中并发和并行编程的不断扩展,程序员越来越沮丧,因为无法使用 Java 构造在各个类的字段上安排原子或有序操作;例如,原子地递增一个count字段。到目前为止,实现这些效果的唯一方法是使用独立的AtomicInteger(增加空间开销和额外的并发问题来管理间接),或者在某些情况下使用原子FieldUpdater(通常会遇到比操作本身更多的开销),或者sun.misc.Unsafe对 JVM 内部函数使用不安全(且不可移植且不受支持)的API。内在函数速度更快,因此它们被广泛使用,但损害了安全性和可移植性。

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

描述

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

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

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

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

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

访问模式分为以下几类:

  1. 读访问模式,例如读取具有易失性存储器排序效果的变量;

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

  3. 原子更新访问模式,例如对读和写具有易失性内存顺序影响的变量进行比较和设置;

  4. 数字原子更新访问模式,例如用于写入的具有简单内存顺序效果的获取和添加以及用于读取的获取内存顺序效果。

  5. 按位原子更新访问模式,例如 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 的查找将在生成并返回 之前,执行与提供对同一字段的读写访问权限的VarHandlea 的查找所执行的访问控制检查完全相同的访问控制检查(代表查找类)MethodHandle(参见类find{,Static}{Getter,Setter}中的方法MethodHandles.Lookup)。

UnsupportedOperationException在以下条件下调用访问模式方法时将抛出异常:

  • 将 a 的访问模式方法写入VarHandle最终字段。

  • 用于引用变量类型或非数字类型(例如)的基于数字的访问模式方法(getAndAdd和) 。addAndGet``boolean

  • 用于引用变量类型或floatdouble类型的基于位的访问模式方法(后一个限制可能会在未来的修订中删除)

无需将字段标记为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

  • 用于引用变量类型或floatdouble类型的基于位的访问模式方法(后一个限制可能会在未来的修订中删除)

变量类型的变量类型(实例字段、静态字段和数组元素)支持所有基本类型和引用类型。其他变量类型可能支持这些类型的全部或子集。

为基于数组视图的变量类型创建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);

相反,agetAndSet需要 2 个参数,一个是接收者的实例Foo,另一个int是要设置的值:

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

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

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

注意:未来的 HotSpot 增强功能可能支持非静态最终字段、方法参数或局部变量中保存的VarHandle、 或实例的常量折叠。MethodHandle

可以使用 为访问模式方法MethodHandle生成A。例如,要为特定变量种类和类型生成“compareAndSet”访问模式:VarHandle``MethodHandles.Lookup.findVirtual``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);

此类MethodHandle查找使用findVirtual将执行asType转换以调整参数和返回值。该行为相当于MethodHandle使用 生成的MethodHandles.varHandleInvoker,类似于 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);

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

访问模式方法调用的源编译将遵循与MethodHandle.invokeExact和 的签名多态方法调用相同的规则MethodHandle.invoke。 Java 语言规范需要添加以下内容:

  1. 参考VarHandle类中的签名多态访问模式方法。
  2. 允许签名多态方法返回 Object 以外的类型,表明返回类型不是多态的(否则将通过调用站点的强制转换来声明)。这使得调用返回 void 的基于写入的访问方法和compareAndSet返回值的调用变得更加容易boolean

期望(但不是要求)增强签名多态方法调用的源编译以执行多态返回类型的目标类型,从而不需要显式强制转换。

注意:用于查找MethodHandleVarHandle利用方法引用语法的语法和运行时支持VarHandle VH_FOO_FIELD_I = Foo::i是可取的,但不在此 JEP 的范围内。

访问模式方法调用的运行时调用将遵循与MethodHandle.invokeExact和 的签名多态方法调用类似的规则MethodHandle.invoke。 Java 虚拟机规范需要添加以下内容:

  1. 参考VarHandle类中的签名多态访问模式方法。
  2. 指定invokevirtual调用访问模式签名多态方法的字节代码行为。预计这种行为可以通过定义从访问模式方法调用到 a 的转换来指定,然后使用相同的参数MethodHandle调用该方法(请参阅前面的使用)。invokeExact``MethodHandles.Lookup.findVirtual

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

  • 驻留在java.lang.invokeHotSpot 将该包中的类的 Final 字段视为真正的 Final 的包中,这VarHandle在静态 Final 字段中引用其自身时启用常量折叠;

  • 利用 JDK 内部注释@Stable对仅更改一次的值进行持续折叠,并@ForceInline确保即使达到正常内联阈值,方法也能内联;和

  • 用于sun.misc.Unsafe底层增强的易失性访问。

一些 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() {}

完整的栅栏比获取栅栏更强(就排序保证而言),获取栅栏比加载栅栏更强。同样,完整的围栏比释放围栏更强,而释放围栏又比商店围栏更强。

可达围栏

可达性栅栏被定义为 上的静态方法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该注释在编译或运行时会导致方法主体按如下方式包装:

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

预计编译时注释处理器可以支持此类功能。

备择方案

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

在讨论这些问题的许多年里,其他几种替代方案,包括基于现场参考的替代方案,已经被提出并被认为在语法、效率和/或可用性方面不可行而被驳回。

在此 JEP 的先前版本中考虑了语法增强,但被认为过于“神奇”,过度使用了volatile浮动接口的关键字范围,一个用于引用,一个用于每种受支持的基本类型。

在此 JEP 的先前版本中考虑了从 扩展的泛型类型,但鉴于未来 Java 版本中的值类型和泛型优于JEPVarHandle的原语,这样的添加(具有泛型类型的增强多态签名和装箱类型变量的特殊处理)被认为是不成熟的218,并使用Arrays 2.0改进了数组。

invokedynamic此 JEP 的先前版本中还考虑了特定于实现的方法。这要求编译后的方法调用有和没有invokedynamic仔细对齐,以使其在语义方面相同。此外,invokedynamic在核心类(例如 say)中使用ConcurrentHashMap会导致循环依赖。

测试

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

风险和假设

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

依赖关系

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

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