1. 背景与动机:我们为何需要虚拟线程?
在很多现代编程语言中,比如 Go 的 Goroutine、C# 的 async/await、Erlang 的进程、Lua 的协程,都存在一种'轻量级线程'或'协程'技术。它们的核心目标是用更低的成本来处理并发,尤其是 I/O 阻塞型操作。
曾几何时,我们 Java 开发者面对这些高效的并发模型,只能羡慕地看着。传统的 Java 平台线程(Platform Thread)与操作系统(OS)线程是 1:1 的映射,这使得它成为一种'重型'资源:
- 创建成本高:每创建一个平台线程,操作系统都需要为其分配一个独立的、通常大小为 1MB 或更多的栈空间。
- 数量有限:受限于内存和操作系统内核参数,一个 JVM 进程通常只能创建几千个平台线程。
- 上下文切换昂贵:线程调度由 OS 内核完成,涉及用户态到内核态的切换,开销较大。
这些限制迫使我们在高并发 I/O 场景下,不得不转向复杂的异步编程范式,比如 CompletableFuture 和响应式编程框架(Reactive Frameworks 如 WebFlux)。虽然这些方案能解决问题,但它们带来了新的挑战:'回调地狱'、陡峭的学习曲线、以及与命令式、顺序化代码风格的割裂,使得代码的编写、调试和维护都变得更加困难。
现在,随着 Java 21 中虚拟线程(Virtual Threads)的正式发布(GA),我们终于可以告别这种两难处境。虚拟线程让我们能够用最直观、最简单的同步阻塞式代码,去实现千万级别的海量并发。
2. 为什么引入虚拟线程?
让我们通过一个故事来理解虚拟线程的价值。
阶段一:同步阻塞
同事接到一个任务:根据前端传来的 fileId,从文件服务器下载文件,解析内容,然后处理。他很快写出了同步代码:
public void doSomething(String fileId) {
String filePath = downloadFile(fileId); // I/O 阻塞
List<XxxDTO> list = readFile(filePath); // I/O 阻塞
doSomething(list);
}
这在请求量小的时候工作得很好。
阶段二:天真的并发
随着业务增长,接口变慢。同事学习了多线程,试图为每个请求创建一个新线程来'提速':
public void doSomething(String fileId) {
new Thread(() -> {
String filePath = downloadFile(fileId);
List<XxxDTO> list = readFile(filePath);
doSomething(list);
}).start();
}
压测时发现,系统很快就因为创建了过多线程而内存溢出(OutOfMemoryError: unable to create new native thread),并且由于频繁的上下文切换,性能不升反降。
阶段三:线程池优化
在大佬指点下,同事学会了使用线程池来复用和管理线程,避免了资源的无限创建:
public void doSomething {
executor.submit(() -> {
downloadFile(fileId);
List<XxxDTO> list = readFile(filePath);
doSomething(list);
});
}



