直接回答:虚拟线程的杀手锏是阻塞时从载体平台线程上卸载(unmount),几乎零成本让出 CPU。但在两种情况下无法卸载:进入 synchronized 块/方法内发生阻塞 IO、或执行 native 方法时——虚拟线程被“钉”在载体线程上(pinning),载体线程连同其上的调度能力一起被阻塞。少量 pinning 无碍,大面积 pinning(如全局 synchronized 锁里做 HTTP 调用)会让虚拟线程退化成平台线程池,并发优势尽失。
展开解析:JDK 21 起 JFR 事件 jdk.VirtualThreadPinned(默认阈值 20ms)可观测 pinning,上线虚拟线程后第一件事就是打开它审计。修复路径:把 synchronized 换成 ReentrantLock(锁实现不钉住,JDK 24 起 JEP 491 已让 synchronized 本身不再 pinning,老版本仍需改造);缩短 synchronized 临界区,把 IO 挪出临界区外。观念纠偏:虚拟线程不是“更快的线程”,是“更便宜的阻塞”——吞吐模型从“池化复用有限线程”变成“一请求一线程”,原来为了省线程而设计的池(连接池仍需,因为它限的是下游资源)与异步回调都可以简化。配套注意:ThreadLocal 在百万级虚拟线程下内存爆炸,改 ScopedValue(JDK 21+);池化虚拟线程是反模式(newVirtualThreadPerTaskExecutor 直接用);阻塞队列生产者消费者跨虚拟/平台线程边界无误。监控上线程数指标失效(会有几万虚拟线程),改看请求并发数与下游连接池水位。
追问方向:JDK 24 的 JEP 491 改了什么?虚拟线程下如何做背压?
(约 490 字)