本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
Java 8 是 2014 年发布的,12 年过去了,还有不少项目和程序员再使用 Java 8,这真的难以想象。
要是一款 AI 大模型,别说 10 年前了,就是去年或前年发布的大模型,现在几乎也凉透了,落后的不能在落后了。
但 Java 8 的魔力就是不一样,是 Java 语言里面最长青的“钉子户”,对虚拟线程之类的毫无兴趣。
说到虚拟线程,JDK 25 中依然存在着与其相关的 Bug,不得已 Java 26 中竟然悄悄的修复了它。
接下来,我们就一起来展开一下 Java 26 中的新改进:虚拟线程类初始化等待不再“钉住”载体线程。这是继 Java 24 JEP 491 之后,虚拟线程(Virtual Threads)领域的又一重要优化。
问题背景
虚拟线程(Virtual Threads)自 Java 21 正式发布以来,被誉为 Java 并发编程的里程碑特性。它允许开发者创建数十万个轻量级线程,而无需为每个线程分配独立的操作系统线程。虚拟线程通过挂载(mount)和卸载(unmount)机制,在少量平台线程(载体线程,carrier thread)上高效调度执行。
然而,虚拟线程的“钉住”问题(Pinning)时有发生,它一直是其规模化应用的绊脚石。所谓“钉住”,是指当虚拟线程执行某些特定操作时,JVM 会强制将其固定在当前的载体线程上,无法卸载。这导致载体线程被阻塞,无法执行其他虚拟线程,严重时可引发“死锁”或“线程饥饿”。
可以说,从 Java 21 到 Java 25,虚拟线程一直陷在“钉住”(Pinning)的困境里。
Java 24 的 JEP 491
要知道 Java 24 通过JEP 491: Synchronize Virtual Threads without Pinning解决了 synchronized 关键字导致的钉住问题。在此之前,虚拟线程在 synchronized 代码块中阻塞时会被钉住,Java 24 后这一限制被解除。
但即便如此,Java 24 仍保留了三种钉住场景。
- 调用本地代码(Native Code)
- 类初始化时的静态代码块执行
- 等待其他线程完成类初始化
而#JDK-8369238正是针对第三种场景的优化。
JDK-8369238 详解
接下来,我们一起来解密 JDK-8369238 是如何让类初始化等待不再钉住的。
问题是什么?
根据 JDK 26 Release Notes 的官方描述。
Virtual Threads Now Unmount from Carrier When Waiting for Another Thread to Execute a Class Initializer
翻译过来就是:
当一个虚拟线程尝试初始化一个正在被其他线程初始化的类时,
在大多数情况下,该虚拟线程现在会从其载体线程上卸载并等待。而在此之前,虚拟线程会被钉住在载体线程上,等待其他线程执行完类初始化器。
为什么这是个问题?
要理解这个问题,需要先了解 Java 的类初始化机制。根据 JVM 规范(JVMS §5.5),类的初始化是线程安全的。也就是说当多个线程同时尝试初始化同一个类时,只有一个线程能执行 <clinit> 方法(类初始化器),其他线程必须等待。
文章配图参考我的原创链接 https://mp.weixin.qq.com/s/rtGevfRovNIvbC2nxWfXUw。
在 Java 26 之前,如果一个虚拟线程(Thread B)发现类 Foo 正在被另一个线程(Thread A)初始化,Thread B 会在 JVM 层面阻塞等待。由于这种等待发生在 JVM 内部,虚拟线程无法卸载,载体线程被钉住。
死锁场景示例
下面看一个老外网友的一个真实的生产环境问题。
- 假设有 16 个载体线程可供虚拟线程调度器使用
- 16 个虚拟线程同时尝试访问某个尚未初始化的类
- 其中一个虚拟线程获得类初始化权,开始执行
<clinit> - 其余 15 个虚拟线程被钉住在各自的载体线程上,等待类初始化完成
- 如果执行
<clinit>的虚拟线程在初始化过程中需要获取某个锁,而这个锁正被其他等待的虚拟线程间接持有(通过复杂的调用链),系统可能陷入死锁
更极端的情况是,如果所有载体线程都被钉住在等待类初始化的虚拟线程上,而执行初始化的线程又需要某个虚拟线程来继续执行(例如,初始化代码中触发了某些异步操作),系统将彻底死锁,没有任何虚拟线程能够继续执行。
JDK 26 是如何解决的?
JDK-8369238 的核心改进是,允许虚拟线程在等待其他线程执行类初始化时,从载体线程上卸载。
具体实现上,JVM 修改了类初始化等待逻辑。
之前:虚拟线程在 JVM 内部阻塞等待,无法卸载,载体线程被占用现在:虚拟线程主动卸载,将载体线程释放给调度器,让其他虚拟线程有机会执行
这一改进与 JEP 491 的理念一脉相承,将虚拟线程的调度权还给 JVM 调度器,而非强制绑定到特定载体线程。
技术细节与限制
官方文档提到in most cases(在大多数情况下),这意味着并非所有类初始化等待场景都能卸载。可能的限制包括以下 3 条。
- 某些特殊的类初始化路径可能仍需要钉住
- 与本地代码交互的类初始化场景
- 涉及复杂同步状态的边界情况
此外,JFR(JDK Flight Recorder)事件 jdk.VirtualThreadPinned 仍然会在这些场景下被触发,帮助开发者诊断剩余的钉住情况。
触发场景与影响分析
哪些场景会触发这个问题?
根据老外网友的反馈,以下代码模式容易触发类初始化等待导致的“钉住”问题。
public class BlockingLoadClass {
static {
// 模拟耗时的类初始化
System.out.println("初始化开始: " + Thread.currentThread());
try {
Thread.sleep(20000); // 20秒初始化时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("初始化结束: " + Thread.currentThread());
}
public static void test() {
System.out.println("测试方法: " + Thread.currentThread());
}
}
// 主程序
public class JavaVirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 平台线程先触发类初始化
new Thread(() -> {
BlockingLoadClass.test();
}).start();
Thread.sleep(1000); // 确保类初始化已开始
// 虚拟线程尝试访问正在初始化的类
Thread.startVirtualThread(() -> {
BlockingLoadClass.test(); // 这里会被钉住(Java 26之前)
});
}
}
典型触发场景包括下面几点。
| 场景 | 描述 |
|---|---|
高并发启动 | 大量虚拟线程同时启动,同时访问尚未初始化的类 |
延迟加载 | 使用懒加载模式的类(如单例模式)在高并发下首次访问 |
框架初始化 | Spring、Hibernate 等框架启动时扫描和初始化大量类 |
动态代理 | 动态生成代理类时的类加载和初始化 |
反射调用 | 通过反射首次访问某个类触发初始化 |
性能影响
在 Java 26 之前,这个问题可能导致下面 3 条性能影响。
- 吞吐量下降:载体线程被无谓占用,无法执行其他虚拟线程
- 延迟增加:等待类初始化的虚拟线程无法及时响应
- 死锁风险:极端情况下系统完全卡死
Java 26 的改进后性能得到改善。
- 载体线程利用率提升:等待期间可执行其他虚拟线程
- 避免死锁:不会因为类初始化等待耗尽所有载体线程
- 更好的可扩展性:高并发场景下虚拟线程调度更灵活
社区实践与建议
如何检测钉住问题?
即使升级到 Java 26,仍然建议使用 JFR 监控虚拟线程钉住情况。
java -XX:StartFlightRecording=settings=profile,filename=recording.jfr \
-Djdk.virtualThreadScheduler.parallelism=16 \
YourApplication
查看 jdk.VirtualThreadPinned 事件,关注 reason 字段。
Native:调用本地代码导致Class initialization:类初始化导致(Java 26 后应减少)Monitor enter:监视器进入(Java 24 后已解决)
迁移建议
官方建议尽快升级到 Java 26 +,如果遇到正在使用虚拟线程并遇到性能或死锁问题。
其次,对于预热类初始化,对于已知的高并发类,可在应用启动时预初始化。
// 启动时强制初始化
Class.forName("com.example.HeavyInitializationClass");
最后,避免在 <clinit> 中执行阻塞操作,这是良好的编程实践,无论使用何种线程模型。
真实案例
国外的 Faire 公司的工程团队分享了他们使用虚拟线程的经验。他们在搜索索引系统中使用虚拟线程,发现 16 个载体线程全部被钉住,导致系统死锁。根本原因就是类初始化等待,所有虚拟线程都在等待一个类的初始化完成,而执行初始化的线程又依赖其他虚拟线程的协作。
他们的临时解决方案是在创建虚拟线程之前,强制预初始化相关类。Java 26 后,这类问题将得到根本性缓解。
总结与展望
JDK-8369238 是虚拟线程生态的又一重要补丁。它解决了 Java 24 之后剩余的钉住场景之一,使虚拟线程在高并发、大量类加载的场景下更加可靠。
改进时间线
| 版本 | 改进内容 |
|---|---|
Java 21 | 虚拟线程正式发布(JEP 444),但 synchronized 和类初始化会钉住 |
Java 24 | JEP 491 解决 synchronized 钉住问题 |
Java 26 | JDK-8369238 解决类初始化等待钉住问题 |
未来展望
目前虚拟线程仅剩的主要钉住场景是调用本地代码(Native Code)。这包括下面 3 点。
- JNI 调用
- Foreign Function & Memory API (FFM) 调用
- 类加载过程中的本地代码(类加载器本身使用本地代码)
随着 Project Loom 的持续推进,我们可以期待未来版本进一步优化这些场景。虚拟线程正在逐步兑现其百万级并发、零成本抽象的承诺。

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » 16 个线程卡死 20 秒,Java 26 偷偷修了个大 Bug,详解 JDK-8369238