Java 21 虚拟线程:使用指南(一)
虚拟线程是由 Java 21 版本中实现的一种轻量级线程。它由 JVM 进行创建以及管理。虚拟线程和传统线程(我们称之为平台线程)之间的主要区别在于,我们可以轻松地在一个 Java 程序中运行大量、甚至数百万个虚拟线程。
由于虚拟线程的数量众多,也就赋予了 Java 程序强大的力量。虚拟线程适合用来处理大量请求,它们可以更有效地运行 “一个请求一个线程” 模型编写的 web 应用程序,可以提高吞吐量以及减少硬件浪费。
由于虚拟线程是 java.lang.Thread 的实现,并且遵守自 Java SE 1.0 以来指定 java.lang.Thread 的相同规则,因此开发人员无需学习新概念即可使用它们。
但是虚拟线程才刚出来,对我们来说有一些陌生。由于 Java 历来版本中无法生成大量平台线程(多年来 Java 中唯一可用的线程实现),已经让程序员养成了一套关于平台线程的使用习惯。这些习惯做法在应用于虚拟线程时会适得其反,我们需要摒弃。
此外虚拟线程和平台线程在创建成本上的巨大差异,也提供了一种新的关于线程使用的方式。Java 的设计者鼓励使用虚拟线程而不必担心虚拟线程的创建成本。
本文无意全面涵盖虚拟线程的每个重要细节,目的只是提供一套介绍性指南,以帮助那些希望开始使用虚拟线程的人充分利用它们。
本文完整大纲如下,

请大方使用同步阻塞 IO
虚拟线程可以显着提高以 “一个请求一个线程” 模型编写的 web 应用程序的吞吐量(注意不是延迟)。在这种模型中,web 应用程序针对每个客户端请求都会创建一个线程进行处理。因此为了处理更多的客户端请求,我们需要创建更多的线程。
在 “一个请求一个线程” 模型中使用平台线程的成本很高,因为平台线程与操作系统线程对应(操作系统线程是一种相对稀缺的资源),阻塞了平台线程,会让它无事可做一直处于阻塞中,这样就会造成很大的资源浪费。
然而,在这个模型中使用虚拟线程就很合适,因为虚拟线程非常廉价就算被阻塞也不会造成资源浪费。因此在虚拟线程出来后,Java 的设计者是建议我们应该以简单的同步风格编写代码并使用阻塞 IO。
举个例子,以下用非阻塞异步风格编写的代码是不会从虚拟线程中受益太多的,
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 编写的代码使用虚拟线程将受益匪浅,
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 命令创建线程转储,
jcmd <pid> Thread.dump_to_file -format=json <file>
用同步风格并使用阻塞 IO 风格编写的代码越多,虚拟线程的性能和可观察性就越好。而用异步非阻塞 IO 风格编写的程序或框架,如果每个任务没有专用一个线程,则无法从虚拟线程中获得显着的好处。
使用虚拟线程,我们因该避免将同步阻塞 IO 与异步非阻塞 IO 混为一谈。
避免池化虚拟线程
关于虚拟线程使用方面最难理解的一件事情就是,我们不应该池化虚拟线程。虽然虚拟线程具有与平台线程相同的行为,但虚拟线程和线程池其实是两种概念。
平台线程是一种稀缺资源,因为它很宝贵。越宝贵的资源就越需要管理,管理平台线程最常见的方法是使用线程池。
不过在使用线程池后,我们需要回答的一个问题,线程池中应该有多少个线程?最小线程数、最大线程数应该设置多少?这也是一个问题。
虚拟线程是一种非常廉价的资源,每个虚拟线程不应代表某些共享的、池化的资源,而应代表单一任务。在应用程序中,我们应该直接使用虚拟线程而不是通过线程池使用它。
那么我们应该创建多少个虚拟线程嘞?答案是不必在乎虚拟线程的数量,我们有多少个并发任务就可以有多少个虚拟线程。
如下是一段提交任务的代码,将每个任务都提交到线程池中执行,在 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 万的并发访问,那么它不太可能从虚拟线程中受益。一方面它负载太轻而不需要更高的吞吐量,一方面并发请求任务也不够多。
参考资料
最后说两句
针对虚拟线程的使用,相信大家心里已经有了答案。虚拟线程不同于平台线程,它非常廉价,Java 的设计者鼓励我们直接使用虚拟线程,而无需池化,也不必担心过多的虚拟现场会影响性能。
事实上,虚拟现场就是为了解决同步阻塞 IO 对硬件的资源利用率不够高这一问题。
关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!
Java 21 虚拟线程:使用指南(一)的更多相关文章
- Java的虚拟线程(协程)特性开启预览阶段,多线程开发的难度将大大降低
高并发.多线程一直是Java编程中的难点,也是面试题中的要点.Java开发者也一直在尝试使用多线程来解决应用服务器的并发问题.但是多线程并不容易,为此一个新的技术出现了,这就是虚拟线程. 传统多线程的 ...
- Java19虚拟线程都来了,我正在写的线程代码会被淘汰掉吗?
Java19中引入了虚拟线程,虽然默认是关闭的,但是可以以Preview模式启用,这绝对是一个重大的更新,今天Java架构杂谈带大家开箱验货,看看这家伙实现了什么了不起的功能. 1 为什么需要虚拟线程 ...
- Java SE 19 虚拟线程
Java SE 19 虚拟线程 作者:Grey 原文地址: 博客园:Java SE 19 虚拟线程 CSDN:Java SE 19 虚拟线程 说明 虚拟线程(Virtual Threads)是在Pro ...
- Java将增加虚拟线程,挑战Go协程
我们知道 Go 语言最大亮点之一就是原生支持并发,这得益于 Go 语言的协程机制.一个 go 语句就可以发起一个协程 (goroutin).协程本质上是一种用户态线程,它不需要操作系统来进行调度,而是 ...
- Java多线程之线程的生命周期
Java多线程之线程的生命周期 一.前言 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态.在线程的生命周期中,它要经过新建(New).就绪(Runnable).运行(R ...
- Java并发编程:线程和进程的创建(转)
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java并发3-多线程面试题
1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速. 2) 线程和进程有什 ...
- Java值创建线程的两种方式对比
在Java中创建线程的方式有两种,第一种是直接继承Thead类,另一种是实现Runable接口.那么这两种方式孰优孰劣呢? 采用继承Thead类实现多线程: 优势:编写简单,如果需要访问当前线程,只需 ...
- Java 并发 中断线程
Java 并发 中断线程 @author ixenos 对Runnable.run()方法的三种处置情况 1.在Runnable.run()方法的中间中断它 2.等待该方法到达对cancel标志的测试 ...
- Java 8并发工具包漫游指南
Java 8并发工具包简介 Java 8并发工具包由3个包组成,分别是java.util.concurrent.java.util.concurrent.atomic和java.util.concur ...
随机推荐
- AT 下分记录
7.30 AGC063 \(+30=1620\) B 做法假 WA 了三次,为啥总是吃了罚时才能发现问题啊 心态还是需要解决的问题.过完 B 啥都想不出来又自闭了
- 解密Prompt系列16. LLM对齐经验之数据越少越好?LTD & LIMA & AlpaGasus
LLM Agent中间插个队,总结下指令微调.对齐数据相关的方案,已经凑够7篇论文可以召唤神龙啦!论文都是以优化指令样本为核心,Data-Centric的观点比较一致:指令微调也就是对齐阶段的数据质量 ...
- 如何使用Java创建数据透视表并导出为PDF
摘要:本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 数据透视分析是一种强大的工具,可以帮助我们从大量数据中提取有用信 ...
- 是因为不同的浏览器内核吗--Could not register service workers到底是怎么回事
什么是浏览器内核 浏览器内核(Rendering Engine),是浏览器最核心的部分. 它负责处理网页的HTML.CSS.JavaScript等代码,并将其转化为可视化的网页内容.即我们常说的对网页 ...
- umicv cv-summary1-全连接神经网络模块化实现
全连接神经网络模块化实现 Linear与Relu单层实现 LossLayer实现 多层神经网络 不同梯度下降方法 Dropout层 今天这篇博文针对Assignment3的全连接网络作业,对前面学习的 ...
- Java代码审计之目录穿越(任意文件下载/读取)
一.目录穿越漏洞 1.什么是目录穿越 所谓的目录穿越指利用操作系统中的文件系统对目录的表示.在文件系统路径中,".."表示上一级目录,当你使用"../"时,你正 ...
- Hyper-V中的虚拟机(Centos)安装FTP服务
linux上是否装上了ftp服务命令: rpm -qa | grep vsftpd ,若没有安装(无显示版本号)则进行下一步 安装ftp服务,命令: yum -y install ftp vsftpd ...
- Go 方法集合与选择receiver类型
Go 方法集合与选择receiver类型 目录 Go 方法集合与选择receiver类型 一.receiver 参数类型对 Go 方法的影响 二.选择 receiver 参数类型原则 2.1 选择 r ...
- 手撕Vuex-提取模块信息
前言 在上一篇[手撕Vuex-模块化共享数据]文章中,已经了解了模块化,与共享数据的注意点. 那么接下来就要在我们自己的 Nuex 中实现共享数据模块化的功能.那么怎么在我们自己的 Nuex 中实现共 ...
- Redis项目搭建
Redis项目搭建 Redis下载 搭建redis首先需要下载Redis,可是Redis官方并没有Windows安装,好在网上从不缺大牛,Github上可以找到Redis的Windows版 下载地址: ...