1. 要点速览
你已经对 java.util.concurrent 包中的类有所了解,尤其是 ExecutorService。它就像一个“任务管理器”:你把工作提交给它(例如通过 submit()),它会自行决定何时以及由哪个线程来执行。通常其底层是固定大小的线程池,从而节省资源,不会为每个任务都新建线程。
然而,有了虚拟线程,一切都变了!现在可以豪横地做到:每个任务一个线程,而且不用担心 JVM 会因为“吃撑”而崩溃。
新方式:Executors.newVirtualThreadPerTaskExecutor()
在 Java 21 中,出现了一种创建 ExecutorService 的新方式,它会为每个任务启动一个独立的虚拟线程:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
重要区别:
- 老的线程池(Executors.newFixedThreadPool、Executors.newCachedThreadPool)会因为操作系统线程开销高而限制并发任务数量。
- 新的虚拟 Executor 几乎不受限:每个任务都有其轻量级的虚拟线程。
简单示例
向虚拟 Executor 提交 10 个任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 10; i++) {
int taskId = i; // 捕获用于 lambda 的变量
executor.submit(() -> {
System.out.println("Task " + taskId + " is running in thread: " +
Thread.currentThread());
});
}
executor.shutdown();
}
}
发生了什么?
每个任务都会在自己的虚拟线程中启动,你会看到类似如下的输出:
Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...
2. 大规模并行:成千上万的任务——不成问题!
为了感受虚拟线程的威力,试着向 ExecutorService 提交不是 10,而是比如 100_000 个任务。在经典线程池中,这就像把大象塞进冰箱:JVM 会很快耗尽内存或变得非常卡顿。虚拟线程则完全不同!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorMassiveDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 100_000; i++) {
int taskId = i;
executor.submit(() -> {
// 作为示例——只睡眠 1 毫秒
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// System.out.println("Task " + taskId + " done."); // 不打印,否则行数会太多!
});
}
executor.shutdown();
}
}
注意:向控制台输出 100_000 行是个坏主意:控制台会比虚拟线程更早“撑不住”。最好不要打印,或只打印前几条任务。
3. newVirtualThreadPerTaskExecutor 如何工作
简而言之:该 ExecutorService 会为你提交的每个任务创建一个新的虚拟线程。与固定大小的线程池不同,它没有任务队列和严格的并发线程数限制(除非受制于你的 JVM 与硬件)。
从架构上看:
- 虚拟线程会被“映射”到少量真正的操作系统(carrier)线程。
- JVM 会自行决定何时启动、挂起与恢复哪个虚拟线程。
- 如果线程被阻塞(例如读取文件或等待网络),JVM 可以“冻结”该虚拟线程,并释放 carrier 线程去处理其他任务。
4. 示例:使用 Future 处理结果
如果任务有返回结果,ExecutorService 会返回一个 Future 对象。这与普通线程的用法相同:
import java.util.concurrent.*;
public class VirtualExecutorWithResult {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(500);
return "Hello from virtual thread!";
});
System.out.println("Result: " + future.get()); // 等待结果
executor.shutdown();
}
}
一切都很熟悉:可以提交有返回值的任务、通过 get() 等待结果,异常处理也与标准流程一致。
5. 正确关闭 Executor
务必记得关闭 ExecutorService,否则程序可能挂起(即便线程是虚拟的而非“真实的”)。
shutdown() 与 awaitTermination
executor.shutdown(); // 表示:不再接受新任务
executor.awaitTermination(1, TimeUnit.MINUTES); // 等待所有任务完成(最多 1 分钟)
为什么这很重要?
如果不调用 shutdown(),虚拟线程可能会继续存活,即使 main() 执行完毕,程序也不会退出。这是新手常见错误。
6. 实用细节
对比:虚拟 Executor vs 经典线程池
| 经典线程池(newFixedThreadPool) | 虚拟 Executor(newVirtualThreadPerTaskExecutor) | |
|---|---|---|
| 线程数量 | 受池大小限制 | 每个任务一个虚拟线程,几乎无限制 |
| 任务排队 | 会的,如果所有线程都忙 | 通常不会:任务会立即获得线程 |
| 线程成本 | 高(栈、操作系统资源) | 很低(由 JVM 负责调度) |
| 可扩展性 | 受限 | 几乎不受限 |
| 适用场景 | CPU 密集型任务、受限并行度 | I/O 密集型任务、大规模并行 |
与 Web 服务器集成
现代 Web 服务器(例如 Tomcat、Jetty、Undertow)已经开始支持虚拟线程。这意味着可以在单独的虚拟线程中处理每个 HTTP 请求,而无需担心在用户激增时“被淹没”。
优势:无需设计复杂的回调式异步方案和 CompletableFuture;代码更简单——可以编写熟悉的阻塞式代码,但应用依然具备良好的可扩展性。
大规模测试与压力仿真
虚拟线程非常适合用于需要“模拟”成千上万并发用户、请求或操作的测试。比如,一个测试向服务器发送 10_000 个并发请求,每个请求在其自己的虚拟线程中处理。
文件与网络连接的并行处理
如果应用处理大量文件或网络连接,可以让每个连接在单独的虚拟线程中处理,无需手动管理线程池。
7. 使用虚拟 Executor 的常见错误
错误 #1:忘记调用 shutdown()。 如果不关闭 Executor,程序将不会退出——虚拟线程仍会等待新任务。必要时请配合 awaitTermination(...) 使用。
错误 №2:把虚拟线程用于重型计算。 虚拟线程并不会加速完全占用 CPU 的任务。对于 CPU 密集型任务,最好使用固定大小的线程池(Executors.newFixedThreadPool),并仔细选择合适的大小。
错误 №3:忽略任务内部的异常。 如果任务抛出异常,它不会自动传到主线程——请通过 Future(方法 get())处理,或在 lambda 内部使用 try/catch。
错误 №4:混淆旧语法/旧 JDK 版本与新特性。 请确认你使用的是正确的 JDK 版本(Java 21+),并确保 IDE 已配置支持虚拟线程。具体方法是 Executors.newVirtualThreadPerTaskExecutor()。
错误 №5:依赖 ThreadLocal 传递上下文。 虚拟线程经常被创建与销毁;ThreadLocal 的行为可能与预期不符。要传递上下文请使用 ScopedValue(Scoped Values;详见下一讲)。
GO TO FULL VERSION