平台线程的天花板
前面二十篇的所有并发手段,都建立在平台线程上——它与操作系统线程 1:1 映射,创建、调度、上下文切换全是内核的活,一条线程默认约 1MB 栈内存。Web 服务「一请求一线程」的模型,在万级并发下只剩两条路:开一万条线程(内存与切换开销爆炸),或者线程池加异步改造(回调地狱、堆栈断裂、可读性崩塌)。瓶颈不在业务,在线程本身太贵。
虚拟线程:让阻塞变得便宜
虚拟线程由 JVM 调度,不直接映射 OS 线程。原理一句话:大量虚拟线程跑在少量载体线程上,某个虚拟线程阻塞时,JVM 把它的栈从载体线程上卸载到堆里,载体线程转身去跑别的虚拟线程;IO 就绪后再装载回来继续跑。阻塞的代价从「占住一条 OS 线程」变成「堆上多存一段栈」——便宜了三个数量级。服务端百万级虚拟线程,不再是新闻。
用法:几乎零成本切换
// 方式一:直接启动
Thread.startVirtualThread(() -> handle(request));
// 方式二:每任务一个虚拟线程的执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> callRemoteApi());
}注意哲学变化:虚拟线程不池化——它便宜到用完即弃,每任务新建一个才是推荐用法。newVirtualThreadPerTaskExecutor 返回的就是「不共享、不复用」的执行器,与第 14 篇的池化哲学正相反——不是线程池过时了,是「为省线程而池化」过时了,普通线程池依然是 CPU 密集任务的主力。
什么时候不该用
虚拟线程救的是 IO 密集:大量阻塞等待的调用。CPU 密集任务用它毫无收益——算力上限依然是核数。两个使用注意:一,不要给虚拟线程再套线程池,违背它的设计;二,早期版本里 synchronized 块内的阻塞会「钉住」(pinning)载体线程,导致栈无法卸载、阻塞退化成占线程——JDK 21 的应对是热点路径改用 ReentrantLock 或避开长阻塞,JDK 24 通过 JEP 491 已彻底修复,synchronized 不再钉住。
它改变了什么
最大的红利是代码形态:「一请求一线程」的同步阻塞写法可以回归——不必为了吞吐把代码改成回调迷宫。简单、线性、可读的阻塞代码,配上百万虚拟线程,这就是范式革命。CompletableFuture 依然是编排好手(组合、超时、异常链),但「为了性能被迫异步」的时代,开始翻篇了。
二十一篇的知识全部到位。下一篇收官:一张并发作战地图,把所有工具按「治哪宗罪、用在什么场景」串成一页纸。
评论 (0)