1.Virtual Thread 改变了什么?
先说重点:
1)Virtual Thread不是“新一代 ThreadPoolExecutor”,而是把原来那个沉重的 Thread 拆成了两层:
- 底层少量 平台线程(OS 线程);
- 上面跑着大量 虚拟线程(Virtual Thread)。
2)对使用者来说,最大变化只有一句话:
开发者可以继续写“看起来是阻塞”的同步代码,但 JVM 会帮你用“协程式”的方式顶住高并发。
换句话说:
1)以前我们都知道:
“每请求一线程”写起来爽,但线程太重,撑不了多少并发。
2)有了虚拟线程之后,JVM 的态度变成了:
“你就放心每请求一线程吧,我把线程变轻一点。”
这会带来几个非常现实的好处:
1)写法不变 / 思维不变
- Controller / Service 里继续用同步风格: userService.findUser(id)、orderService.listOrders(id),没有 Mono、CompletableFuture 满天飞。
2)并发能力上一个数量级
- 原来你不敢把线程池开到几千、几万;
- 现在用虚拟线程,几万、几十万“每请求一线程”都可以认真考虑。
3)大部分普通 Web / RPC 服务,没那么刚需 WebFlux/Netty 了
- 不是说 WebFlux/Netty 没用了,而是:
终于可以用“最简单的写法”解决绝大多数高并发场景。
某位网友说过一句有点夸张又有点道理的话:
“一个 socket 对应一个虚拟线程就是最佳方案。”
这句话“最佳”二字有点吹过头,但有一点是对的:
在今天的 Java 里,“一个连接/请求一个虚拟线程”确实很可能是新的合理默认。
1. 回到老问题:为什么以前“每请求一线程”顶不住?
传统 Spring MVC / Tomcat 的模型,大概是这样:
- 客户端发来一个 HTTP 请求;
- Tomcat 从线程池里拿出一个 平台线程(OS 线程) 交给这个请求;
- 你的 Controller 里写一堆同步代码,里面各种阻塞调用:
@GetMapping("/user/{id}")
public UserProfile getUser(@PathVariable String id) {
// 阻塞式 HTTP 调用
User user = restTemplate.getForObject("http://user-service/" + id, User.class);
// 阻塞式数据库调用
List<Order> orders = jdbcTemplate.query("SELECT * FROM orders WHERE user_id = ?", rs -> {
// ...
}, id);
return new UserProfile(user, orders);
}
看起来很优雅,对吧?
问题是:这个请求在等待下游服务的时候,那个 OS 线程也在那儿干等。
一个线程默认栈空间几 MB;线程多了以后:
- 内存被吃光;
- 上下文切换越来越频繁,CPU 时间都浪费在切线程上。
所以才会有这些“补救方案”:
1)调大 Tomcat 线程池:顶一阵,但迟早顶不住;
2)把阻塞调用改成异步:
- Callable / DeferredResult / CompletableFuture / WebFlux;
- 代码复杂度和心智负担一起上来。
抽象成一句话:
以前的“每请求一线程”模式,在高并发时代最大的问题就是:
线程太贵,阻塞太浪费。
于是大家才开始写:
- sendAsync(...);
- thenApply(...) / thenCompose(...);
- Mono.zip(...) / flatMap(...)。
能顶住并发没错,但可读性、可维护性确实打折扣。
2. Virtual Thread 是什么?和普通 Thread 有啥不一样?
先用一句话把定义讲清楚:
平台线程(Platform Thread) ≈ OS 线程
虚拟线程(Virtual Thread) = JVM 自己调度的“用户态线程”
2.1 关键差异:谁来调度、谁在“阻塞”
传统 Thread:
一个 Thread 就是一个 OS 线程;
当你在里面调 socket.read()、jdbc、HttpClient.send():
- 线程卡在内核里等待 I/O;
- 整个 OS 线程被占住,啥也干不了。
虚拟线程:
虚拟线程自己并不直接对应 OS 线程;
它们是跑在少量 carrier threads(承载线程) 上的一堆“轻量任务”;
当虚拟线程遇到支持 Loom 的阻塞 I/O 时,JVM 会做两件事:
- 把这

最低0.47元/天 解锁文章
1292

被折叠的 条评论
为什么被折叠?



