跳到主要内容

JEP 312:线程本地握手

QWen Max 中英对照 JEP 312 Thread-Local Handshakes

概述

引入一种在不执行全局虚拟机安全点的情况下在线程上执行回调的方法。使得停止单个线程变得既可行又高效,而不仅仅是停止所有线程或一个都不停。

非目标

在所有受支持的架构上高效地实现这一点可能并不可行。最初的目标并不是支持所有的处理器架构以及处理器架构的所有版本。

成功指标

  • 新机制在标准基准测试中产生的性能开销不超过 1%。

  • 新机制不会增加到达传统全局安全点所需的时间。

动机

能够停止单个线程有着众多的应用:

  • 改进偏向锁撤销机制,使其仅停止单个线程以撤销偏向,而不是停止所有线程。

  • 减少不同类型的服务性查询对虚拟机整体延迟的影响,例如在拥有大量 Java 线程的虚拟机上获取所有线程的堆栈跟踪可能是一项缓慢的操作。

  • 通过减少对信号的依赖来执行更安全的堆栈跟踪采样。

  • 使用所谓的非对称 Dekker 同步技术,通过与 Java 线程进行握手,省略一些内存屏障。例如,G1 和 CMS 所需的条件卡标记代码将不需要内存屏障。因此,G1 的写后屏障可以得到优化,并且可以移除那些试图避免内存屏障的分支。

所有这些都将通过减少全局安全点的数量来帮助虚拟机实现更低的延迟。

描述

握手操作是一种回调,它在每个 JavaThread 处于安全点(safepoint)状态时执行。该回调要么由线程自身执行,要么由 VM 线程在保持线程阻塞状态下执行。安全点与握手之间的主要区别在于,每线程的操作会尽快在所有线程上执行,并且一旦自己的操作完成,线程就可以立即继续执行。如果已知某个 JavaThread 正在运行,则也可以对该单个 JavaThread 执行握手操作。

在最初的实现中,将会有一个限制,即在任意给定时间最多只能进行一个握手操作。然而,该操作可以涉及所有 JavaThreads 中的任意子集。VM 线程将通过一个 VM 操作来协调握手操作,这实际上会防止在握手操作期间发生全局安全点。

当前的安全点机制被修改为通过每个线程的指针执行一次间接操作,这将允许单个线程的执行被强制在保护页上陷入。本质上,在任何时候都会有两个轮询页:一个始终受到保护,另一个则始终不设保护。为了迫使某个线程让出资源,虚拟机(VM)会更新对应线程的每个线程指针,使其指向受保护的页面。

线程本地握手最初将在 x64 和 SPARC 上实现。其他平台将回退到正常的 safepoint。一个新的产品选项 -XX:ThreadLocalHandshakes(默认值 true),允许用户在支持的平台上选择正常的 safepoint。

替代方案

考虑了多种替代方案:

  • 改为发出条件分支。这会消耗分支预测器的状态,并且不如简单的加载操作紧凑。在这一领域的实验表明,条件分支的性能可能高度依赖于目标 CPU 的具体微架构。条件分支方法的另一个缺点是,每个条件分支安全点都需要输出一个相应的存根,以处理返回到轮询位置的问题。

  • 有一种想法是牺牲另一个寄存器,然后将该寄存器所保存的地址加载到寄存器本身,假设寄存器的内容是其自身线程本地字段的地址。可以通过将字段更改为 NULL 来启动线程本地握手。在下一次轮询时,寄存器将被设置为 NULL,而在第二次轮询时,加载操作将触发陷阱。这种方法需要全局牺牲一个寄存器,陷阱的开销更大,并且平均而言,在发出线程停止请求后,需要两倍的轮询次数才能到达安全点。其优点是理论上对应用程序执行的影响较小。

  • 此前构建了一个原型,其中全局轮询页面保持不变,但只有实际的目标线程会被捕获到虚拟机代码中。非目标线程则会简单地从信号处理器返回并继续执行。这种方法的一个缺点是,如果目标线程响应缓慢,则可能会导致其他 Java 线程出现信号风暴,因为在目标线程响应之前,无法解除轮询页面的武装。