Skip to content

JEP 312: Thread-Local Handshakes | 线程本地握手

摘要

介绍一种在线程上执行回调的方法,而无需进行全局 VM 安全点操作。使停止单个线程成为可能且廉价,而不仅仅是停止所有线程或不停止任何线程。

非目标

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

成功指标

  • 新机制在标准基准测试中不会产生超过 1% 的性能开销。
  • 新机制不会增加达到传统全局安全点所需的时间。

动机

能够停止单个线程有多种应用:

  • 改进偏向锁撤销,只停止单个线程来撤销偏向,而不是全部线程。
  • 减少不同类型服务性查询对整体 VM 延迟的影响,例如获取所有线程的堆栈跟踪,在具有大量 Java 线程的 VM 上可能是一个缓慢的操作。
  • 通过减少对信号的依赖,执行更安全的堆栈跟踪采样。
  • 使用称为非对称 Dekker 同步技术来省略一些内存屏障,通过与 Java 线程进行握手。例如,G1 需要的条件卡标记代码和 CMS 使用的代码将不需要内存屏障。因此,G1 的后写屏障可以进行优化,并且可以删除试图避免内存屏障的分支。

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

描述

握手操作是在每个 JavaThread 处于安全点安全状态时执行的回调。该回调由线程自身执行,或者由 VM 线程在将线程保持在阻塞状态时执行。安全点和握手之间的一个重大区别是,针对每个线程的操作将尽快在所有线程上执行,并且它们将在自己的操作完成后继续执行。如果已知某个 JavaThread 正在运行,则也可以与该单个 JavaThread 执行握手。

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

当前的安全点方案被修改为通过一个每个线程指针进行间接引用,从而允许强制单个线程的执行在监视页面上触发。基本上,始终会存在两个轮询页面:一个总是受保护的页面,一个总是不受保护的页面。为了强制线程让出,VM 更新相应线程的每个线程指针,使其指向受保护的页面。

首先在 x64 和 SPARC 上实现线程本地握手。其他平台将回退到正常的安全点。新的产品选项 -XX:ThreadLocalHandshakes(默认值为 true)允许用户在支持的平台上选择正常的安全点。

替代方案

考虑了多种替代方案:

  • 发出条件分支。这会消耗分支预测器状态,并且不像加载操作那样紧凑。在这个领域的实验表明,条件分支的性能可能高度依赖于目标 CPU 的具体微架构。条件分支方法的另一个缺点是,每个条件分支安全点都需要相应的存根来处理返回到轮询位置的情况。
  • 有一个想法是牺牲另一个寄存器,然后对寄存器所持有的地址执行加载操作,假设寄存器的内容是其自己线程本地字段的地址。通过将字段更改为 NULL 来启动线程本地握手。下一次轮询时,寄存器将被设置为 NULL,第二次轮询时,加载操作将触发陷阱。这需要在全局范围内牺牲一个寄存器,陷阱的代价更高,并且平均需要两倍的轮询次数才能达到安全点,一旦请求停止线程。好处是理论上对应用程序执行的影响较小。
  • 以前曾构建过一个原型,其中全局轮询页面保留不变,但只有实际的目标线程会被 VM 代码捕获。不是握手目标的线程将从信号处理程序返回并继续执行。这种方法的缺点是,如果目标线程响应速度较慢,则其他 Java 线程可能会受到信号风暴的影响,因为无法在目标线程响应之前解除轮询页面的武装状态。