同步调用把平台线程池堵满了,Virtual Threads 能接着用这种写法吗?

同步接口把平台线程池占满,再加线程也补不过来。请求还在往里进,每个都要等下游返回。池子调大,操作系统线程先顶到上限,处理器却往往还闲着。有人会把这段逻辑拆成回调或响应式管道,只为把那条昂贵的平台线程让出去。想继续用「一个请求一条线程」的写法,又希望别被操作系统里能开的数量卡住,官方后来给出的是另一种实现,而不是再发明一套并发语法。

JEP 444 的状态是 Closed / Delivered,随 JDK 21 交付。它把虚拟线程写成由 JDK 提供的轻量实现:同样是 java.lang.Thread 的实例,却不在整个生命周期里独占一条操作系统线程。平台线程仍是操作系统线程的薄封装,数量受其限制。虚拟线程只在真正做计算时占用底层线程,一旦调用阻塞式 I/O,运行时会把这条虚拟线程挂起,把载体腾给别的任务。Java SE 25 的虚拟线程文档 用虚拟内存作类比:运行时把大量虚拟线程映射到少量操作系统线程上。该页写明,它们适合大部分时间在等待的任务,而不是长时间占着处理器的计算。并且「它们并不是更快的线程」,要的是更高吞吐,不是更低的单次延迟。公开的 Thread 构造方法仍然创建平台线程,并不会因为启用了这项能力就悄悄改掉旧写法。

启用方式也不另起一套 API。文档给出 Thread.ofVirtual().start(...),也给出 Executors.newVirtualThreadPerTaskExecutor():每提交一个任务就新建一条虚拟线程,而不是从池子里借用。JEP 和这份指南都强调,虚拟线程不该被池化——它们本来就不贵,数量应对准并发任务本身。若要把对某个下游的并发限制在固定上限,文档建议用信号量这类专用结构,而不是再搭一个虚拟线程池。把原来的固定大小平台线程池原样换成同等数量的虚拟线程,官方也写了,几乎得不到好处。要改的是每个并发任务有没有自己的一条线程,不是给池子里那几十条换个名字。采纳指南还提到一条经验:若应用里的虚拟线程数量从来到不了约一万这个量级,多半是负载还轻,或仍有大量任务没被表示成线程,这时收益有限。指南也提醒,不要把同步、阻塞的代码和异步框架混在一起写,后一种风格往往并没有为每个任务准备一条专用线程,收益会不明显。

Java SE 25 这份文档在调度一节写了:虚拟线程若被钉在载体上,阻塞时就无法卸载。该页列出的钉住情形,是执行 native 方法或外来函数(Foreign Function and Memory API)。钉住不会让结果算错,但可能妨碍扩展。这份文档不再把 synchronized 写进钉住条件。JEP 444 作为 JDK 21 的交付说明里,相关表述更早,和现行这页并不完全相同。另外,文档提醒不要把昂贵、可复用又非线程安全的对象缓存在线程局部变量里——虚拟线程不会在无关任务之间复用,这类缓存会适得其反。观察方面,调试器可以单步进入虚拟线程。也可用 jcmd <pid> Thread.dump_to_file 导出文本或 JSON 格式的转储,其中会列出平台线程和虚拟线程,但不包含传统转储里的对象地址等信息。

虚拟线程仍然是 Java 的 Thread。站内之前谈过 Swoole 协程和 goroutine 的对照,那是另一组并发模型的比较。虚拟线程并不是把那些模型搬进来,只是让「一个请求一条线程」这种写法,在实现上撑得住更高的并发。

你们同步接口把池子堵住的时候,是先改任务怎么提交,还是已经在看框架会不会给每个请求开一条虚拟线程?

踩过,而且踩得挺惨。

原来那段同步调用换成虚拟线程以后,吞吐根本没起来。后来才想明白问题不在池子,在下游连接数。虚拟线程不排队了,请求全冲过去把数据库连接池压死,报错比之前还多。

加了个信号量限住对下游的并发,才正常。

官方文档那句「它们并不是更快的线程」是真的,它让等待变便宜,不是让下游变快。