Java 虚拟线程为何仍会阻塞,但不再长时间占用平台线程

作者:菠萝爱吃肉2026.11.07 02:27浏览量:0

简介:虚拟线程常被误解为“不会阻塞”,更准确的理解是:阻塞仍会发生,但在支持卸载的调用路径上,挂起的是虚拟线程,平台线程可被释放去执行其他任务。本文从一次 socket 读取入手,对比平台线程与虚拟线程的调度差异,说明阻塞、卸载、继续执行之间的关系,并总结引入虚拟线程后需要重新审视的性能问题。

一、问题的来源:虚拟线程真的“不阻塞”吗

在 Java 21 引入虚拟线程之后,“虚拟线程不再有阻塞问题”成为常见说法。这个表述很容易让人误以为虚拟线程从根本上消除了阻塞,好像调用一个尚未返回的方法,后面的业务代码还能继续向下执行。事实上,只要代码是顺序执行的,调用没有返回,后续逻辑就必须等待。虚拟线程并没有改变 Java 语言的基本执行语义。

更准确的理解是:虚拟线程仍然会阻塞,但阻塞的成本模型发生了变化。对于普通平台线程来说,阻塞不仅意味着当前任务暂停,还意味着承载该任务的底层操作系统线程被占用。大量阻塞操作会导致平台线程资源迅速耗尽,系统无法调度更多就绪任务。而虚拟线程的设计目标之一,就是在发生可卸载的阻塞时,让底层平台线程不被无谓占用。

二、从一次 socket 读取看传统平台线程的阻塞

以一次常见的 socket 读取为例,调用方通过输入流读取网络数据。当数据尚未到达时,读取方法不会返回。使用传统平台线程执行这段代码时,会发生以下过程:

  1. 调用线程进入阻塞式读取。
  2. 操作系统发现当前 socket 没有可读数据。
  3. 线程被挂起,等待 I/O 事件。
  4. 线程不再持续消耗 CPU,但线程栈、线程状态以及相关资源仍然被保留。
  5. 其他任务无法使用这个平台线程,直到该线程被唤醒并继续执行。

这个过程中,CPU 未必繁忙,但平台线程资源却在持续占用。如果系统中有大量这样的阻塞请求,即使很多任务只是在等待外部事件,平台线程也可能被占满。继续创建更多平台线程会带来内存、调度和上下文切换成本,因此传统模型中往往需要引入线程池、异步编程或响应式框架来规避平台线程的过度占用。

三、虚拟线程下的同一段阻塞代码

换成虚拟线程后,业务代码仍然使用相同的阻塞式读取写法,但运行行为发生了变化。当虚拟线程调用一个支持卸载的阻塞操作时,过程可能如下:

  1. 虚拟线程发起 socket 读取。
  2. 数据尚未到达,虚拟线程被挂起。
  3. JVM 识别到该阻塞点可以被卸载,于是释放承载该虚拟线程的平台线程。
  4. 被释放的平台线程返回到调度队列,继续执行其他就绪的虚拟线程。
  5. 当网络数据到达后,原先挂起的虚拟线程重新变为可运行状态。
  6. 调度器将其安排到某个可用平台线程上,从阻塞点之后继续执行。

关键区别不在业务代码是否等待,而在于等待期间是否继续占用平台线程。虚拟线程从代码层面仍然是顺序阻塞的,但从资源调度层面看,虚拟线程的阻塞不再等同于平台线程的阻塞。

四、阻塞与卸载并不是同一件事

要准确理解虚拟线程,需要把“阻塞”和“卸载”区分开。

阻塞是业务语义层面的概念,表示当前调用尚未完成,后续代码不能继续执行。卸载是运行时调度层面的概念,表示虚拟线程在阻塞期间是否释放了底层平台线程。

并非所有阻塞都能自动完成卸载。例如,在同步锁内部执行阻塞式 I/O,或者某些没有为虚拟线程提供卸载支持的 native 调用,可能导致虚拟线程持有锁并阻塞,同时底层平台线程也被占用。因此,虚拟线程可以降低大量 I/O 密集场景下的资源占用,但不能简单认为所有阻塞都会被自动消除。

五、平台线程与虚拟线程的资源占用对比

为了更直观地说明差异,可以用一个简单示意代码表示阻塞读取:

  1. try (Socket socket = new Socket()) {
  2. socket.connect(new InetSocketAddress(host, port));
  3. InputStream input = socket.getInputStream();
  4. byte[] buffer = new byte[1024];
  5. // 数据未到达时,当前线程会一直等待
  6. int count = input.read(buffer);
  7. // 只有 read 返回后,后续逻辑才会执行
  8. process(buffer, count);
  9. }

在平台线程模型中,执行这段代码的线程在 read 等待期间被占用。在虚拟线程模型中,只要该阻塞点支持卸载,执行这段代码的虚拟线程虽然也在等待,但底层平台线程可以被释放出来处理其他虚拟线程。

这就意味着,同样是几万个并发连接,使用平台线程可能需要几万个操作系统线程;而使用虚拟线程时,只需要少量平台线程就可以承载大量处于等待状态的虚拟线程。

六、CPU 与线程资源之间的关系

需要注意的是,虚拟线程并不会让等待本身消失,也不意味着 CPU 消耗会下降。如果一个任务本来就需要等待网络数据 100 毫秒,那么无论使用平台线程还是虚拟线程,业务结果的返回时间都不会缩短。

虚拟线程主要解决的是“等待期间大量线程资源被占用”的问题。它更适合 I/O 密集、并发量大、单个任务阻塞时间较长但计算量不大的场景。对于 CPU 密集型任务,虚拟线程并不能带来明显收益,因为这类任务本来就需要持续占用 CPU,平台线程必须始终处于运行状态。

七、需要重新审视的问题

引入虚拟线程后,开发人员仍然需要关注阻塞点是否支持卸载。例如,某些 synchronized 块中发生长期阻塞,可能继续占用平台线程。此时可以考虑使用支持更细粒度并发的锁,或者尝试缩小同步块范围。

另一方面,虚拟线程虽然降低了创建成本,但也不能无节制地创建。大量虚拟线程同时等待外部事件时,虽然平台线程负担不大,但每个虚拟线程仍然会占用一部分内存用于保存栈和状态信息。同时,大量任务被唤醒后,可能产生突发的 CPU 和内存压力。因此,在极高并发场景下,仍需要结合压测结果评估容量。

八、总结

“Java 21 虚拟线程不再有阻塞问题”略过了关键条件。更准确的表述是:虚拟线程仍然会阻塞,业务代码仍然需要等待调用返回;但在支持卸载的阻塞路径上,虚拟线程的等待不再长时间占用平台线程。

这种变化带来的收益不是消灭等待,而是让底层的平台线程在虚拟线程等待时继续服务其他任务,从而显著提升系统的并发承载能力,尤其适合大量 I/O 等待场景。理解这一点后,开发人员可以更合理地使用虚拟线程,并继续关注锁竞争、卸载支持和容量压测等问题,而不是简单地把虚拟线程当作所有并发问题的万能替代方案。