前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Java 21 虚拟线程:使用指南(一)

Java 21 虚拟线程:使用指南(一)

原创
作者头像
wayn
发布于 2024-01-01 08:09:33
发布于 2024-01-01 08:09:33
98400
代码可运行
举报
文章被收录于专栏:wayn的程序开发wayn的程序开发
运行总次数:0
代码可运行

虚拟线程是由 Java 21 版本中实现的一种轻量级线程。它由 JVM 进行创建以及管理。虚拟线程和传统线程(我们称之为平台线程)之间的主要区别在于,我们可以轻松地在一个 Java 程序中运行大量、甚至数百万个虚拟线程。

由于虚拟线程的数量众多,也就赋予了 Java 程序强大的力量。虚拟线程适合用来处理大量请求,它们可以更有效地运行 “一个请求一个线程” 模型编写的 web 应用程序,可以提高吞吐量以及减少硬件浪费。

由于虚拟线程是 java.lang.Thread 的实现,并且遵守自 Java SE 1.0 以来指定 java.lang.Thread 的相同规则,因此开发人员无需学习新概念即可使用它们。

但是虚拟线程才刚出来,对我们来说有一些陌生。由于 Java 历来版本中无法生成大量平台线程(多年来 Java 中唯一可用的线程实现),已经让程序员养成了一套关于平台线程的使用习惯。这些习惯做法在应用于虚拟线程时会适得其反,我们需要摒弃。

此外虚拟线程和平台线程在创建成本上的巨大差异,也提供了一种新的关于线程使用的方式。Java 的设计者鼓励使用虚拟线程而不必担心虚拟线程的创建成本。

本文无意全面涵盖虚拟线程的每个重要细节,目的只是提供一套介绍性指南,以帮助那些希望开始使用虚拟线程的人充分利用它们。

本文完整大纲如下,

请大方使用同步阻塞 IO

虚拟线程可以显着提高以 “一个请求一个线程” 模型编写的 web 应用程序的吞吐量(注意不是延迟)。在这种模型中,web 应用程序针对每个客户端请求都会创建一个线程进行处理。因此为了处理更多的客户端请求,我们需要创建更多的线程。

在 “一个请求一个线程” 模型中使用平台线程的成本很高,因为平台线程与操作系统线程对应(操作系统线程是一种相对稀缺的资源),阻塞了平台线程,会让它无事可做一直处于阻塞中,这样就会造成很大的资源浪费。

然而,在这个模型中使用虚拟线程就很合适,因为虚拟线程非常廉价就算被阻塞也不会造成资源浪费。因此在虚拟线程出来后,Java 的设计者是建议我们应该以简单的同步风格编写代码并使用阻塞 IO。

举个例子,以下用非阻塞异步风格编写的代码是不会从虚拟线程中受益太多的,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CompletableFuture.supplyAsync(info::getUrl, pool)
   .thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofString()))
   .thenApply(info::findImage)
   .thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofByteArray()))
   .thenApply(info::setImageData)
   .thenAccept(this::process)
   .exceptionally(t -> { t.printStackTrace(); return null; });

另一方面,以下用同步风格并使用阻塞 IO 编写的代码使用虚拟线程将受益匪浅,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
try {
   String page = getBody(info.getUrl(), HttpResponse.BodyHandlers.ofString());
   String imageUrl = info.findImage(page);
   byte[] data = getBody(imageUrl, HttpResponse.BodyHandlers.ofByteArray());
   info.setImageData(data);
   process(info);
} catch (Exception ex) {
   t.printStackTrace();
}

并且上面的同步代码也更容易在调试器中调试、在分析器中分析或通过线程转储进行观察。要观察虚拟线程,可以使用 jcmd 命令创建线程转储,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
jcmd <pid> Thread.dump_to_file -format=json <file>

用同步风格并使用阻塞 IO 风格编写的代码越多,虚拟线程的性能和可观察性就越好。而用异步非阻塞 IO 风格编写的程序或框架,如果每个任务没有专用一个线程,则无法从虚拟线程中获得显着的好处。

使用虚拟线程,我们因该避免将同步阻塞 IO 与异步非阻塞 IO 混为一谈。

避免池化虚拟线程

关于虚拟线程使用方面最难理解的一件事情就是,我们不应该池化虚拟线程。虽然虚拟线程具有与平台线程相同的行为,但虚拟线程和线程池其实是两种概念。

平台线程是一种稀缺资源,因为它很宝贵。越宝贵的资源就越需要管理,管理平台线程最常见的方法是使用线程池。

不过在使用线程池后,我们需要回答的一个问题,线程池中应该有多少个线程?最小线程数、最大线程数应该设置多少?这也是一个问题。

虚拟线程是一种非常廉价的资源,每个虚拟线程不应代表某些共享的、池化的资源,而应代表单一任务。在应用程序中,我们应该直接使用虚拟线程而不是通过线程池使用它。

那么我们应该创建多少个虚拟线程嘞?答案是不必在乎虚拟线程的数量,我们有多少个并发任务就可以有多少个虚拟线程。

如下是一段提交任务的代码,将每个任务都提交到线程池中执行,在 Java 21 以后,不建议再使用共享线程池执行器,代码如下,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Future<ResultA> f1 = sharedThreadPoolExecutor.submit(task1);
Future<ResultB> f2 = sharedThreadPoolExecutor.submit(task2);
// ... use futures

建议使用虚拟线程执行器,代码如下,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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() 比较合适,在这种场景中,希望同时对不同的服务执行多个传出调用,并且方法结束时就关闭线程池,代码如下,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

最后说两句

针对虚拟线程的使用,相信大家心里已经有了答案。虚拟线程不同于平台线程,它非常廉价,Java 的设计者鼓励我们直接使用虚拟线程,而无需池化,也不必担心过多的虚拟现场会影响性能。

事实上,虚拟现场就是为了解决同步阻塞 IO 对硬件的资源利用率不够高这一问题。

我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量
我们常用的 Java 线程与系统内核线程是一一对应的,系统内核的线程调度程序负责调度 Java 线程。为了增加应用程序的性能,我们会增加越来越多的 Java 线程,显然系统调度 Java 线程时,会占据不少资源去处理线程上下文切换。
PPPHUANG
2022/09/21
1K0
Java 21正式发布 小小使用一下期待已久的虚拟线程
👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.
Lorin 洛林
2023/11/08
6980
Java 21正式发布 小小使用一下期待已久的虚拟线程
每天一个Java面试题之虚拟线程
虚拟线程是 Java 21 中最为重要的特性。Java 从 Java 19 开始引入虚拟线程,在 Java 21 中就正式升级为正式特性。可见官方也把虚拟线程作为 Java 21 长久支持版本的吸引点。虚拟线程是轻量级的线程,可以在显著的减少代码编写的同时提高系统的吞吐量。
灬沙师弟
2024/08/20
1450
每天一个Java面试题之虚拟线程
Java面试高频知识点:OOP、集合、多线程与虚拟线程如何备考?
未来趋势: Java 多线程与集合体系将持续优化,从 JDK 8 的 Lambda 表达式到 JDK 21 的虚拟线程,技术进步为开发者提供了更多选择。通过不断实践与学习,你将更自信地应对 Java 面试!
猫头虎
2024/12/26
960
Java 19 发布,Loom 怎么解决 Java 的并发模型缺陷?
作者 | Deepu K Sasidharan 译者 | 张卫滨 策划 | 褚杏娟 本文最初发表于 okta 网站,经原作者 Deepu K Sasidharan 授权由 InfoQ 中文站翻译分享,未经许可禁止转载。 Java 19 已经于日前发布,其中最引人注目的特性就要数虚拟线程了,本文介绍了 Loom 项目中虚拟线程和结构化编程的基础知识,并将其与操作系统线程进行了对比分析。 Java 在其发展早期就具有良好的多线程和并发能力,能够高效地利用多线程和多核 CPU。Java 开发工具包(Ja
深度学习与Python
2023/03/29
6760
Java 19 发布,Loom 怎么解决 Java 的并发模型缺陷?
JDK21 新特性分析,但我用Java8
在此之前,我们创建线程后需要销毁线程来释放内存,会造成大量的成本消耗,完美的解决方案就是我们用线程池,通过线程池来管理线程,并且共享线程相当于说我用的时候去租用一个线程。
小熊学Java
2023/11/03
1.3K0
JDK21 新特性分析,但我用Java8
Java 中的线程池简介-Java快速进阶教程
本教程介绍了 Java 中的线程池。我们将从标准Java库中的不同实现开始,然后查看Google的Guava库。
jack.yang
2025/04/05
660
Java 中的线程池简介-Java快速进阶教程
ExecutorService 并发指南
在软件开发不断发展的世界中,有效管理并发任务的能力至关重要。传统的线程方法可能变得繁琐且容易出错,特别是在处理大量异步操作时。这时,ExecutorService 登场了:它是Java并发框架中一个强大的抽象,旨在简化和优化异步任务执行。
FunTester
2025/01/23
2130
ExecutorService 并发指南
CompletableFuture 使用介绍
本文安利一个 Java8 的工具 CompletableFuture,这是 Java8 带来的一个非常好用的用于异步编程的类。
明明如月学长
2021/08/31
9160
Java19的新特性
上面列出的是大方面的特性,除此之外还有一些api的更新及废弃,主要见JDK 19 Release Notes,这里举几个例子。
code4it
2022/09/21
4930
Java19的新特性
《面试补习》- 多线程知识梳理
常用的方式主要由上述3种,需要注意的是 使用 ,而不是创建线程,从实现的代码我们可以看到,Java 创建线程只有一种方式, 就是通过 new Thread() 的方式进行创建线程。
九灵
2021/06/28
3100
《面试补习》- 多线程知识梳理
学生:什么是 CompletableFuture 啊?啪!老师甩过来一篇文章
文章来源:https://javadoop.com/post/completable-future
cxuan
2020/12/17
9270
学生:什么是 CompletableFuture 啊?啪!老师甩过来一篇文章
Java ExecutorService:你真的了解它吗?
生动形象的比喻,ExecutorService 就像是一个管理者,你可以把任务交给它,它会根据需要创建线程,并且确保任务按照你的要求执行。
忆愿
2025/01/21
1040
Java ExecutorService:你真的了解它吗?
Java一分钟之线程池:ExecutorService与Future
在Java并发编程的世界里,线程池是提高程序性能、管理线程生命周期的利器。ExecutorService与Future作为Java并发包中的核心组件,它们不仅简化了多线程编程的复杂度,还为我们提供了强大的异步执行和结果获取能力。本文将深入浅出地探讨这两个概念,揭示常见问题、易错点及避免策略,并辅以代码示例,帮助大家更好地理解和应用。
Jimaks
2024/05/15
3240
Java一分钟之线程池:ExecutorService与Future
Java中的线程池与Executor框架详解
Java中的线程池是通过Executor框架来实现的,Executor框架提供了一系列的接口和类来简化线程池的使用和管理。下面将详细介绍Java中线程池的相关概念和Executor框架的主要组成部分。
用户1289394
2024/04/03
1340
Java中的线程池与Executor框架详解
再见了Future,图解JDK21虚拟线程的结构化并发
Java为我们提供了许多启动线程和管理线程的方法。在本文中,我们将介绍一些在Java中进行并发编程的选项。我们将介绍结构化并发的概念,然后讨论Java 21中一组预览类——它使将任务拆分为子任务、收集结果并对其进行操作变得非常容易,而且不会不小心留下任何挂起的任务。
JavaEdge
2023/12/11
2.1K0
再见了Future,图解JDK21虚拟线程的结构化并发
java线程池(一):java线程池基本使用及Executors
在前面学习线程组的时候就提到过线程池。实际上线程组在我们的日常工作中已经不太会用到,但是线程池恰恰相反,是我们日常工作中必不可少的工具之一。现在开始对线程池的使用,以及底层ThreadPoolExecutor的源码进行分析。
冬天里的懒猫
2020/09/17
1.4K0
java线程池(一):java线程池基本使用及Executors
相关推荐
Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验