Java 21 神仙特性:虚拟线程使用指南( 二 )


虚拟线程是一种非常廉价的资源,每个虚拟线程不应代表某些共享的、池化的资源,而应代表单一任务 。在应用程序中,我们应该直接使用虚拟线程而不是通过线程池使用它 。
那么我们应该创建多少个虚拟线程嘞?答案是不必在乎虚拟线程的数量 , 我们有多少个并发任务就可以有多少个虚拟线程 。
如下是一段提交任务的代码,将每个任务都提交到线程池中执行 , 在 Java 21 以后,不建议再使用共享线程池执行器,代码如下,
Future<ResultA> f1 = sharedThreadPoolExecutor.submit(task1);Future<ResultB> f2 = sharedThreadPoolExecutor.submit(task2);// ... use futures建议使用虚拟线程执行器 , 代码如下,
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {Future<ResultA> f1 = executor.submit(task1);Future<ResultB> f2 = executor.submit(task2);// ... use futures}上面代码虽然仍使用 ExecutorService,但从 Executors.newVirtualThreadPerTaskExecutor() 方法返回的执行器不再使用线程池 。它会为每个提交的任务都创建一个新的虚拟线程 。
此外 , ExecutorService 本身是轻量级的,我们可以像创建任何简单对象一样直接创建一个新的 ExecutorService 对象而不必考虑复用 。
这使我们能够依赖 Java 19 中新添加的 ExecutorService.close() 方法和 try-with-resources 语法糖 。在 try 块末尾隐式调用 ExecutorService.close() 方法,会自动等待提交给 ExecutorService 的所有任务(即 ExecutorService 生成的所有虚拟线程)终止 。
对于广播场景来说,使用 Executors.newVirtualThreadPerTaskExecutor() 比较合适,在这种场景中,希望同时对不同的服务执行多个传出调用 , 并且方法结束时就关闭线程池,代码如下,
void handle(Request request, Response response) {var url1 = ...var url2 = ...try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {var future1 = executor.submit(() -> fetchURL(url1));var future2 = executor.submit(() -> fetchURL(url2));response.send(future1.get() + future2.get());} catch (ExecutionException | InterruptedException e) {response.fAIl(e);}}String fetchURL(URL url) throws IOException {try (var in = url.openStream()) {return new String(in.readAllBytes(), StandardCharsets.UTF_8);}}

针对广播模式和其他常见的并发模式 , 如果希望有更好的可观察性 , 建议使用结构化并发 。这是 Java 21 中新出的特性,这里给大家卖个关子,我将在后续进行讲解 。
根据经验来说,如果我们的应用程序从未经历 1 万的并发访问 , 那么它不太可能从虚拟线程中受益 。一方面它负载太轻而不需要更高的吞吐量,一方面并发请求任务也不够多 。
参考资料
  • https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-E695A4C5-D335-4FA4-B886-FEB88C73F23E




推荐阅读