Java 21 探讨虚拟线程锁在哪里?

介绍

Netflix 在广泛的微服务架构中一直将 Java 作为主要编程语言。随着我们使用更新版本的 Java,JVM 生态系统团队会寻找可以改善我们系统的人机工程学和性能的新语言特性。在最近的一篇文章中,我们详细描述了当我们迁移到 Java 21 并将代际 ZGC 作为默认垃圾收集器时,我们的工作负载如何受益。虚拟线程是我们在此次迁移中兴奋采用的另一项功能。

对于虚拟线程的新手,它们被描述为“轻量级线程,大大减少了编写、维护和观察高吞吐量并发应用程序的工作量。”它们的强大之处在于,当发生阻塞操作时,它们能够通过继续执行自动挂起和恢复,从而释放底层操作系统线程以供其他操作使用。在适当的上下文中利用虚拟线程可以解锁更高的性能。

在本文中,我们讨论了在部署 Java 21 上的虚拟线程过程中遇到的一个奇特案例。

问题

Netflix 工程师向性能工程和 JVM 生态系统团队提出了几份独立的报告,报告称出现了间歇性超时和挂起的实例。在仔细检查后,我们注意到了一些共同的特征和症状。在所有情况下,受影响的应用程序都运行在 Java 21、SpringBoot 3 上,并嵌入了 Tomcat 在 REST 端点上服务流量。经历问题的实例虽然 JVM 仍在运行,但却停止了流量服务。这个问题的一个明显症状是 closeWait 状态下的套接字数量持续增加,如下图所示:

收集的诊断信息

处于 closeWait 状态的套接字表明远程对等端关闭了套接字,但本地实例从未关闭,可能是因为应用程序未能这样做。这通常表明应用程序处于异常挂起状态,在这种情况下,应用程序线程转储可能会揭示更多信息。

为了排除这个问题,我们首先利用我们的警报系统来捕捉处于这种状态的实例。由于我们定期收集并持久化所有 JVM 工作负载的线程转储,我们通常可以通过检查这些实例的线程转储来追溯行为。然而,我们惊讶地发现我们所有的线程转储都显示 JVM 完全闲置,没有明显的活动。回顾最近的更改,发现这些受影响的服务启用了虚拟线程,而我们知道虚拟线程调用栈不会出现在 jstack 生成的线程转储中。为了获得包含虚拟线程状态的更完整的线程转储,我们使用了 “jcmd Thread.dump_to_file” 命令。作为检查 JVM 状态的最后一搏,我们还从实例中收集了堆转储。

分析

线程转储显示数千个“空白”虚拟线程:

arduino复制代码#119821 "" virtual
​
#119820 "" virtual
​
#119823 "" virtual
​
#120847 "" virtual
​
#119822 "" virtual
...

这些是为其创建了线程对象但尚未开始运行的 VTs(虚拟线程),因此没有堆栈跟踪。实际上,空白 VTs 的数量与 closeWait 状态下的套接字数量大致相同。要理解我们所看到的内容,我们首先需要了解 VTs 如何运行。

虚拟线程不是与专用的 OS 级线程一对一映射的。相反,我们可以将其视为调度到 fork-join 线程池的任务。当虚拟线程进入阻塞调用(例如等待 Future)时,它会放弃占用的 OS 线程,并在内存中保留,直到准备好恢复。在此期间,OS 线程可以重新分配以执行相同 fork-join 池中的其他 VTs。这使我们能够将大量 VTs 复用到少数几个底层 OS 线程中。在 JVM 术语中

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

幻想多巴胺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值