ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

虚拟线程-JDK21之后并发的换轨

虚拟线程-JDK21之后并发的换轨

虚拟线程:JDK 21 之后并发的换轨

一万个连接一万个线程的账,以前的解法是 NIO 和多路复用,让一个线程看一群连接。JDK 21 给了第二个答案,把线程便宜到一万起步,阻塞代码随便写。虚拟线程改变的是价格,执行速度一寸没变。把这笔账算清,就知道它替你省了什么、又埋了什么坑。

平台线程贵在哪

普通线程也叫平台线程,一个线程一个 OS 调度单位,栈内存一兆起步,创建和上下文切换都要进内核。一万个平台线程,光栈就吃掉几个 G,切换开销把 CPU 吃满,这就是 BIO 篇算过的账。贵的根源是「线程绑定 OS 资源」。

虚拟线程便宜在哪

虚拟线程是 JVM 自己调度的轻量任务。它跑在少量平台线程(载体线程)上,遇到阻塞就从载体上下来,把载体让给别人,恢复时再找一个载体接着跑。栈按需增长,一个虚拟线程的开销是千字节级,创建十万个也只是内存里的一堆小对象。JEP 444 在 JDK 21 把它转正。

少量载体线程

十万个虚拟线程

阻塞就换人

任务1

任务2

……

平台线程

平台线程

用法一行。Executors.newVirtualThreadPerTaskExecutor(),每个任务一个虚拟线程,或者Thread.ofVirtual().start(...)。这里有个反直觉的规矩,虚拟线程不要池化,它便宜到不需要复用,池化反而把它的优势锁死。

三个注意

第一,pinning 的旧账。虚拟线程在 synchronized 块里阻塞时,曾经会把载体线程一起钉住,外面的任务全等着,这是虚拟线程早期最大的坑。JEP 491「Synchronize Virtual Threads without Pinning」把这个问题解掉了(落地版本以 JEP 页为准,核验 2026-10-25)。老版本上跑高并发,synchronized 是雷,新版本上这颗雷已拆。

第二,ThreadLocal 的账。虚拟线程数量一大,每个都持有一份 ThreadLocal 就是灾难,连接池里的 ThreadLocal 尤其危险。上下文传递改用 ScopedValue(预览特性)或者显式传参,是新姿势。

第三,CPU 密集型无收益。虚拟线程省的是「等」的时间,计算密集的任务该多核还是多核,换轨道不改变物理。

亲手数一数

下面用一段 Java 对照创建十万虚拟线程和五千平台线程的时间与内存。直接运行java VirtualBill.java(JDK 21+,推荐 24 以上)。

// 运行:java VirtualBill.java// 对照虚拟线程与平台线程的创建账单。importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.Executors;publicclassVirtualBill{staticlongusedHeapMB(){Runtimer=Runtime.getRuntime();return(r.totalMemory()-r.freeMemory())/(1024*1024);}publicstaticvoidmain(String[]args)throwsException{intvirtualCount=100_000;longbefore=usedHeapMB();longstart=System.nanoTime();try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){CountDownLatchlatch=newCountDownLatch(virtualCount);for(inti=0;i<virtualCount;i++){executor.submit(()->{try{Thread.sleep(10);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}latch.countDown();});}latch.await();}longms=(System.nanoTime()-start)/1_000_000;System.out.printf("虚拟线程 %d 个:%d ms,堆 +%d MB%n",virtualCount,ms,usedHeapMB()-before);intplatformCount=5_000;before=usedHeapMB();start=System.nanoTime();Thread[]ts=newThread[platformCount];CountDownLatchlatch=newCountDownLatch(platformCount);for(inti=0;i<platformCount;i++){ts[i]=newThread(()->{try{Thread.sleep(10);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}latch.countDown();});ts[i].start();}latch.await();for(Threadt:ts){t.join();}ms=(System.nanoTime()-start)/1_000_000;System.out.printf("平台线程 %d 个:%d ms,堆 +%d MB%n",platformCount,ms,usedHeapMB()-before);System.out.println("数量差 20 倍,账单差得更多,这就是价格差。");}}

适用边界

这一篇管虚拟线程的机制与用法。结构化并发、作用域值的完整 API,以及 Web 容器(Tomcat 2024 起支持)上的实测表现,是另外的题。和线程池篇对起来读最有意思,线程池的七个参数是在为「线程贵」做精算,虚拟线程时代那套精算大半作废,但池化思维里的拒绝、排队、隔离,换个形态依然存在。跑完演示再看你的服务,阻塞点满天飞的 I/O 密集服务是第一批换轨的候选,改一行启动代码就能试。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进