虚拟线程是JVM用户态协程,不注册到OS内核,由ForkJoinPool调度,一个OS线程可承载成千上万个;阻塞操作需JVM识别挂起,否则卡住载体线程。

虚拟线程不是“轻量级 OS 线程”
Java 虚拟线程(VirtualThread)根本不在操作系统内核里注册,它只是 JVM 用户态调度的协程。OS 线程(Thread)每个都对应一个 pthread 或 Windows 线程,要进内核排队、占栈、参与 CPU 时间片竞争;而虚拟线程由 JVM 的 ForkJoinPool(默认 carrier pool)在用户态批量调度,一个 OS 线程可承载成千上万个虚拟线程。
常见错误现象:Thread.currentThread().getName() 返回类似 VirtualThread[#123]/runnable,但你以为它能直接调用 Thread.sleep(1000) 就阻塞自己?错——它会把整个载体线程卡住,导致其他虚拟线程饿死。
- 真正阻塞操作(如
Object.wait()、Thread.sleep()、LockSupport.park())必须由 JVM 识别并挂起虚拟线程,同时释放载体线程去跑别的任务 - 只有 JDK 内置 I/O(如
Files.readAllBytes()、ServerSocket.accept())和明确标注@Blocking的 API 才支持自动挂起;自定义 native 调用或 JNI 不触发挂起 - 用
Thread.ofVirtual().unstarted(Runnable)启动的才是虚拟线程;直接 newThread(Runnable)还是传统线程
载体线程被阻塞时会发生什么
虚拟线程依赖“载体线程”(carrier thread)执行实际 CPU 工作,但载体线程本身是普通 OS 线程。一旦你在虚拟线程里调用了未被 JVM 拦截的阻塞调用(比如老式 InputStream.read() 同步阻塞、或 System.in.read()),JVM 无法感知,只能让该载体线程原地等待,直到阻塞结束。
使用场景:HTTP 客户端用 HttpClient(JDK 11+)没问题,它内部已适配虚拟线程;但若用 Apache HttpClient 4.x 或 OkHttp 3.x,默认仍是阻塞 I/O,会拖垮载体线程池。
立即学习“Java免费学习笔记(深入)”;
- 错误示例:
new BufferedReader(new InputStreamReader(System.in)).readLine()—— 在虚拟线程中执行会导致载体线程永久挂起 - 正确做法:改用
AsynchronousFileChannel、HttpClient.newHttpClient(),或显式用Executors.newCachedThreadPool()把阻塞操作扔给传统线程池 - 可通过 JVM 参数
-Djdk.virtualThreadCarrierThreadKeepAliveMillis=1000控制空闲载体线程存活时间,避免资源滞留
上下文切换开销真的消失了吗
虚拟线程确实消除了 OS 级上下文切换(从用户态到内核态再切回来),但没消灭“调度决策”和“栈保存/恢复”的成本。JVM 仍需在虚拟线程间切换执行权,只是换成了用户态跳转,且栈是堆上分配的 Continuation 对象,不是固定大小的内核栈。
性能影响:创建百万级虚拟线程几乎无压力(Thread.start() 耗时 ~100ns),但频繁在大量虚拟线程间轮转(如每毫秒唤醒一个)会加重 JVM 调度器负担,反而不如固定几个 OS 线程 + 异步回调模型。
- 别盲目替换所有
ExecutorService:高吞吐、低延迟、计算密集型任务仍适合传统线程池 +ForkJoinPool - 虚拟线程最适合 I/O 密集、请求-响应生命周期短、并发连接数高的场景(如 WebFlux 替代方案、gRPC 服务端)
- 监控关键指标:用
jcmd <pid> VM.native_memory summary查看Internal内存增长;用jstack -l <pid>观察虚拟线程状态是否大量卡在WAITING (parking)而非RUNNABLE
为什么 ThreadLocal 在虚拟线程里默认不继承
传统 ThreadLocal 绑定的是 OS 线程实例,而虚拟线程每次可能被不同载体线程执行,所以默认不会跨虚拟线程传递值。这不是 bug,是设计选择:避免隐式状态污染和内存泄漏。
容易踩的坑:Spring 的 RequestContextHolder、Logback 的 MDC 都依赖 ThreadLocal,直接在虚拟线程中用会丢失上下文。
- 解决方案一:用
InheritableThreadLocal+Thread.Builder.inheritInheritableThreadLocals(true)显式开启继承(仅限 JDK 21+) - 解决方案二:手动在虚拟线程启动前
copy关键ThreadLocal值,例如MDC.getCopyOfContextMap()+MDC.setContextMap(...) - 注意:
ThreadLocal的remove()必须显式调用,否则虚拟线程退出后其持有的对象可能长期滞留在载体线程的ThreadLocalMap中
while(true) { Thread.sleep(1); } 在虚拟线程里,照样卡死整个载体池。







