tomcat 10.1+原生支持虚拟线程执行器,但启用后吞吐不升反降的主因是阻塞调用未让出载体线程:thread.sleep()、synchronized块或同步jdbc/http调用会卡死底层平台线程,需改用jdk 21+异步api并验证jstack中virtualthread状态及forkjoinworkerthread数量。

Tomcat 10.1+ 原生支持虚拟线程作为请求执行器,但直接启用不等于自动解决线程耗尽——关键在**阻塞调用是否真正让出载体线程**。若业务代码里混着 Thread.sleep()、synchronized 块或未适配的 JDBC 驱动,虚拟线程仍会挂起并卡住底层平台线程,吞吐反而下降。
Tomcat 10.1+ 启用虚拟线程执行器必须绕过默认 Connector 配置
Tomcat 默认的 Executor 是平台线程池(如 org.apache.tomcat.util.threads.ThreadPool),即使 JDK 是 21,它也不会自动切到虚拟线程。你得显式替换为自定义 Executor 实现:
- 不要改
server.xml里的<executor></executor>标签——它只认平台线程池类型 - 必须在 Spring Boot 应用中通过
WebServerFactoryCustomizer<tomcatservletwebserverfactory></tomcatservletwebserverfactory>注入自定义Connector,并在其setProtocol("org.apache.coyote.http11.Http11Nio2Protocol")后手动设置setExecutor(new VirtualThreadExecutor()) -
VirtualThreadExecutor实现不能只是Thread.ofVirtual().start(runnable),要确保异常不逃逸导致线程泄漏,建议用try { ... } finally { Thread.currentThread().interrupt(); }包裹
数据库/HTTP 客户端调用必须用异步 API,否则虚拟线程白搭
虚拟线程在 FileChannel.read()、HttpClient.sendAsync() 等 JDK 21+ 异步 I/O 方法上能自动挂起;但对传统 Connection.createStatement().executeQuery() 或 RestTemplate.getForObject() 这类同步阻塞调用,JVM 无法感知,会把整个载体线程堵死。
- PostgreSQL 需用
pgjdbc-ng或asyncpg(非官方)等真正异步驱动;标准postgresql-42.x驱动仍是同步模型,虚拟线程调它等于白换 - HTTP 调用必须弃用
RestTemplate和OkHttp同步接口,改用HttpClient.newBuilder().build().sendAsync() - Redis 推荐
lettuce的StatefulRedisConnection#async(),别用jedis
验证是否真“不卡”:别只看线程数,要看挂起状态和载体线程复用率
启动后光看 jstack 里 java.lang.VirtualThread 数量没意义——大量 WAITING 状态且堆栈含 Unsafe.park,说明被阻塞住了;而真正的健康状态是:多数虚拟线程处于 RUNNABLE 或刚完成的 TERMINATED,且 jcmd <pid> VM.native_memory summary</pid> 显示平台线程数稳定在几十个(默认 ForkJoinPool 并行度)。
- 用
jstack <pid> | grep -A5 "VirtualThread.*WAITING" | head -20</pid>快速筛出可疑挂起点 - 检查堆栈是否落在
java.net.SocketInputStream.socketRead0或java.sql.DriverManager.getConnection——这些就是没走异步路径的铁证 - 如果发现
ForkJoinWorkerThread数量持续增长(> 2×CPU 核数),说明载体线程被长期占用,调度器被迫扩容,已偏离虚拟线程设计本意
最易被忽略的一点:虚拟线程不是“开了就赢”,它把调度权交给了 JVM,但业务代码里的同步原语(synchronized、ReentrantLock.lock())、ThreadLocal 泄漏、甚至日志框架的 MDC 绑定,都会让挂起/恢复失效,最终退化成平台线程行为。上线前务必用真实 IO 压测 + jstack + GC 日志三者交叉验证。










