导读:本期聚焦于布兰登创作的《虚拟线程遇到阻塞IO时如何自动挂起并让出carrier thread?》,敬请观看详情。线程在执行网络请求或文件读写时一旦发生阻塞,传统平台线程只能干等着白白占用系统资源,而虚拟线程却能在阻塞瞬间自动挂起,把底层的carrier thread释放给其他任务使用。这背后的实现离不开JDK对阻塞点的改造、Continuation的栈冻结与拷贝以及调度器的再入队机制。本文从HotSpot虚拟机的源码视角出发,逐层拆解虚拟线程挂起的完整链路,分析park、yield与IO阻塞三种场景的差异,并给出堆栈拷贝开销、synchronized pinning等常见问题的排查思路,帮助你真正理解虚拟线程的运行原理。

虚拟线程是JDK 19引入并在JDK 21正式转正的轻量级并发方案。它最吸引人的特性之一就是:当代码执行到阻塞IO时,虚拟线程会自动挂起自己,把承载它的carrier thread让出来去运行其他虚拟线程。这意味着你可以放心地写出阻塞式代码,却获得接近异步编程的吞吐量。要理解这个魔法是怎么发生的,需要深入到Continuation、调度器和JDK类库改造这三个层面。

虚拟线程遇到阻塞IO时如何自动挂起并让出carrier thread?

一、挂起的基石:Continuation的栈冻结与恢复

虚拟线程之所以能够挂起后再恢复,核心依赖是HotSpot内部实现的Continuation机制。每个虚拟线程在Java堆上保存着自己的执行栈(默认大小由虚拟线程自身决定,不再受平台线程栈的限制),当虚拟线程被挂起时,JVM会执行一次「栈冻结」操作:把当前线程栈上的所有栈帧从线程本地栈拷贝到堆上的Continuation对象中,随后carrier thread的栈被清空,可以继续执行其他任务。

当这个虚拟线程被重新调度执行时,JVM执行反向操作,把堆中的栈帧「解冻」并拷回carrier thread的栈上,从中断的那条指令处继续执行。整个冻结和解冻过程对应用代码完全透明,你在调用socket.read()时根本感知不到栈被搬来搬去。这种机制本质上实现了「有栈协程」,与Kotlin协程的无栈方案(通过状态机改造方法体)形成了鲜明对比。

栈拷贝是有成本的。如果虚拟线程的调用栈很深(比如经过多层框架封装后调用链达到上百个栈帧),每次挂起和恢复都需要拷贝更多数据。这也是官方建议虚拟线程配合短小任务、避免过深调用链的原因之一。可以通过JFR事件jdk.VirtualThreadPinnedjdk.VirtualThreadSubmitFailed来观测挂起行为是否异常频繁。

二、阻塞点的改造:从java.net到java.nio的全面接管

Continuation只提供了「能挂起」的能力,而「什么时候该挂起」则依赖JDK类库的全面改造。在早期Loom原型中,开发团队改造了大量会发生阻塞的JDK API,包括java.net.Socketjava.net.ServerSocketjava.io.FileInputStream等传统的阻塞式IO类。当虚拟线程调用这些API进入阻塞时,类库不再让底层的carrier thread傻等,而是把IO操作注册到内部的轮询器(Poller)上,然后调用VirtualThread.parkNanos()主动挂起。

以一次HTTP调用为例,虚拟线程在发出请求后会经历这样的链路:JDK内部把连接的读写事件注册到一个基于epoll或kqueue实现的Poller线程上,随后虚拟线程挂起,carrier thread立即回到ForkJoinPool调度器去领取下一个可运行的虚拟线程。当远端数据到达、Poller检测到fd就绪时,它会调用unpark()把对应的虚拟线程重新加入调度队列,等待被某个carrier thread捡起来继续执行。整个过程没有任何线程真正阻塞在IO上。

文件IO则稍有不同,因为操作系统的文件读写通常不支持epoll风格的事件通知。JDK的兜底方案是使用一个补偿性的线程池:当虚拟线程执行文件IO时,操作会被转交到专门的COMPENSATOR线程池中执行,虚拟线程照样挂起,carrier thread照样被释放,只是IO本身还是由真实线程完成的。这解释了为什么大量小文件读写场景下虚拟线程的收益不如网络IO那么显著。

三、park、yield与pinning:三种让出场景的区别

除了IO阻塞,虚拟线程还有几种让出carrier thread的途径,它们的触发条件和底层行为并不相同。Thread.sleep()BlockingQueue.take()LockSupport.park()都会触发挂起,本质上是走到Continuation.yield()这个入口。而Thread.yield()Thread.onSpinWait()则相对轻量,只是提示调度器重新排队。

最需要警惕的是pinning问题。当虚拟线程在执行synchronized修饰的方法或代码块期间发生阻塞,或者在挂起时栈帧中存在本地方法调用,JVM无法完成栈冻结,此时虚拟线程会被「钉」在carrier thread上,真正阻塞的是底层平台线程。这在高并发下会导致carrier thread被大量占用,吞吐量不升反降。JDK 24已经对synchronized的pinning问题做了优化,但如果你使用的是JDK 21,就应该把热点路径上的同步块改造成ReentrantLock。可以通过启动参数-Djdk.tracePinnedThreads=full来定位pinning发生的堆栈位置。

排查示例代码如下:

// 启动参数加 -Djdk.tracePinnedThreads=full 可打印钉住位置
var vt = Thread.ofVirtual().start(() -> {
    synchronized (lock) {
        try {
            // 在synchronized内阻塞,JDK 21会pin住carrier thread
            socket.getInputStream().read(buffer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
});

// 推荐改造方式:使用ReentrantLock替代synchronized
private final ReentrantLock lock = new ReentrantLock();

public void handle(Socket socket) throws IOException {
    lock.lock();
    try {
        socket.getInputStream().read(buffer); // 可正常挂起
    } finally {
        lock.unlock();
    }
}

四、调度器如何衔接:ForkJoinPool的两级队列模型

挂起之后的故事由调度器接手。虚拟线程默认使用一个共享的ForkJoinPool(可通过jdk.virtualThreadScheduler.parallelism调整并行度,默认等于CPU核数),它工作在FIFO模式下。每个carrier thread维护一个本地队列,同时还有一个全局的提交队列。被unpark()唤醒的虚拟线程会被放入调度队列,等待任意空闲的carrier thread来执行,虚拟线程与carrier thread之间是多对多的动态绑定关系,不保证恢复后跑在原来那个carrier上。

这种设计带来一个重要推论:任何依赖ThreadLocal传递上下文的代码在虚拟线程上都要格外小心。虽然ThreadLocal在虚拟线程上是可用的,但由于实例数量可能达到百万级,缓存类ThreadLocal会造成严重的内存压力。JDK提供了ScopedValue作为更合适的不可变上下文传递方案,配合结构化并发使用效果更好。

总结一下完整的挂起链路:应用代码触发阻塞点,改造后的JDK API检测到当前是虚拟线程,将IO事件注册到Poller或补偿线程池,随后调用park触发Continuation栈冻结,carrier thread归还调度器执行其他任务;IO就绪后Poller调用unpark,虚拟线程重新入队,被某个carrier thread恢复执行。理解了这条链路,你就能解释虚拟线程的大部分行为特征,也能在出现性能问题时快速定位是pinning、栈拷贝开销还是调度饥饿导致的。

虚拟线程挂起机制carrier thread修改时间:2026-09-05 17:44:54

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260905/51048.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。