再见了Future,图解JDK21虚拟线程的结构化并发

Java为我们提供了许多启动线程和管理线程的方法。在本文中,我们将介绍一些在Java中进行并发编程的选项。我们将介绍结构化并发的概念,然后讨论Java 21中一组预览类——它使将任务拆分为子任务、收集结果并对其进行操作变得非常容易,而且不会不小心留下任何挂起的任务。

1 基础方法

通过Lambda表达式启动平台线程的这种创建线程的方法最简单,适用于简单情况。

// Lambda表达式启动平台线程的一种方法。
Thread.ofPlatform().start(() -> {// 在这里执行在独立线程上运行的操作});

问题

  • 创建平台线程是昂贵的
  • 若应用程序用户量很大,平台线程数量可能增长到超出JVM支持的限制

显然,大多数应用程序服务器不鼓励这种行为。因此,继续下一种方法——Java Futures。

2 Java Future类

JDK 5引入,开发者需要改变思考方式。不再考虑启动新线程,而考虑将“任务”提交到线程池以供执行。JDK 5还引入ExecutorService,任务将提交到该服务。ExecutorService是一个定义了提交任务并返回Java Future的机制的接口。提交的任务需实现Runnable或Callable接口。

任务提交给表示单线程线程池

// 将Callable任务提交给表示单线程线程池的ExecutorServiceExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(() -> {// 进行一些工作并返回数据return "Done";
});
// 在这里执行其他任务// 阻塞直到提交的任务完成
String output = future.get();// 打印 "Done"
System.out.println(output);// 继续执行后续任务

多个任务提交到ExecutorService

try (ExecutorService service = Executors.newFixedThreadPool(3)) {Future<TaskResult> future1 = service.submit(() -> { // 执行任务1并返回TaskResult });Future<TaskResult> future2 = service.submit(() -> { // 执行任务2并返回TaskResult });  Future<TaskResult> future3 = service.submit(() -> { // 执行任务3并返回TaskResult });/* 所有异常上抛 */// get()将阻塞直到任务1完成TaskResult result1 = future1.get();// get()将阻塞直到任务2完成TaskResult result2 = future2.get();// get()将阻塞直到任务3完成TaskResult result3 = future3.get();// 处理result1、result2、result3handleResults(result1, result2, result3);
}

所有这些任务将并行运行,然后父线程可用future.get()方法检索每个任务的结果。

3 上述实现的问题

如在上面代码中用Platform线程,则存在一个问题。获取TaskResult的get()方法将阻塞线程,由于与阻塞Platform线程相关的可扩展性问题,这代价可能很昂贵。然而,使用Java 21——如用Virtual Threads,则在get()期间,底层的平台线程不会被阻塞。

若task2、task3在task1前完成,须等到task1完成,然后处理task2和task3结果。

若task2或task3执行过程失败,则问题更糟。假设整个用例应在任何任务失败时就失败,代码将等到task1完成,然后抛异常。这不是我们的期望,它将为最终用户创建一个非常迟钝的体验。

3.1 基本问题

ExecutorService类对提交给它的各种任务之间关系一无所知。因此,它不知道若一个任务失败,该发生点啥。即示例中提交的三个任务被视为独立任务,而非用例的一部分。这并不是ExecutorService类的失败,因为它没有设计为处理提交的任务之间的任何关系。

3.2 另一个问题

ExecutorService周围使用try-with-resources块,确保在try块退出时调用ExecutorService的close方法。close方法确保所有提交给执行器服务的任务在继续执行之前终止。

若用例要求在任何任务失败时立即失败,那我们运气不好。close方法将等待所有提交的任务完成。

但若不用try-with-resources块,则不能保证在块退出前三个任务都结束。将保留未清理终止的“未明确终止的线程”。任何其他自定义实现都须确保在失败时立即取消其他任务。

因此,尽管用Java Future是处理可拆分为子任务的任务的一种不错方法,但还不够完美。开发须将用例的“感知”编码到逻辑中,但这很难!

注意,对Platform线程存在于Java Futures的问题之一即阻塞问题——Java 21使用Virtual线程时,这问题不再存在。因为使用Virtual Threads时,使用future.get()方法阻塞线程将释放底层的Platform线程。

使用CompletableFuture Pipelines也可解决阻塞问题,但这里不深入探讨。有更简单的方法来解决Java 21阻塞问题,没错就是Virtual Threads!但我们需要找到一种更好解决方案,以处理可拆分为多个子任务且“知道”用例的任务。这就引出结构化并发的基本思想。

4 结构化并发

想象,从方法内部向ExecutorService提交的任务,然后方法退出。现在更难推断代码,因为不知道此提交的任务可能的副作用,且这可能导致难以调试的问题。该问题的图解:

结构化并发基本思想是从一个块(方法或块)内启动的所有任务应在该块结束前终止。即:

  • 代码的结构边界(块)

  • 和该块内提交的任务的运行时边界

重合。这使应用程序代码更容易理解,因为一个块内提交的所有任务的执行效果都被限制在该块内。块外查看代码时,不必担心任务是否仍在运行。

ExecutorService的try-with-resources块是对结构化并发的一次良好尝试,其中从块内提交的所有任务在块退出时完成。但它还不够,因为它可能导致父线程等待时间超过必要时间。其改进版——StructuredTaskScope

5 StructuredTaskScope

Java 21 Virtual Thread作为一项功能被引入,它在大多情况下实际上消除了阻塞问题。但即使使用Virtual线程和Futures,仍存在“不干净终止任务”和“等待时间比必要时间长”的问题。

StructuredTaskScope类在Java 21中作为预览功能提供,旨在解决这问题。它试图提供比Executor Service的try-with-resources块更干净的结构化并发模型。StructuredTaskScope类知道提交的任务之间的关系,因此它可对它们进行更智能假设。

使用StructuredTaskScope的示例

在任一任务失败时,立即返回用例。

StructuredTaskScope.ShutdownOnFailure()返回一个StructuredTaskScope的引用,该引用知道若一个任务失败,那其他任务也须终止,因为它“知道”提交的任务之间的关系。

 try(var scope = new StructuredTaskScope.ShutdownOnFailure()) {          // 想象一下LongRunningTask实现Suppliervar dataTask = new LongRunningTask("dataTask", ...);  var restTask = new LongRunningTask("restTask", ...); // 并行运行任务Subtask<TaskResponse> dataSubTask = scope.fork(dataTask);           Subtask<TaskResponse> restSubTask = scope.fork(restTask);           // 等待所有任务成功完成或第一个子任务失败。 // 如果一个失败,向所有其他子任务发送取消请求// 在范围上调用join方法,等待两个任务都完成或如果一个任务失败scope.join();                                                       scope.throwIfFailed();                                              // 处理成功的子任务结果                                System.out.println(dataSubTask.get());                              System.out.println(restSubTask.get());                              }                                                                       

企业用例

其中两个任务可并行运行:

  • 一个DB任务
  • 一个Rest API任务

目标是并行运行这些任务,然后将结果合并到单个对象中并返回。

调用ShutdownOnFailure()静态方法创建一个StructuredTaskScope类。然后使用StructuredTaskScope对象fork方法(将fork方法考虑为submit方法)并行运行两个任务。幕后,StructuredTaskScope类默认使用Virtual线程来运行任务。每次fork一个任务,都创建一个新Virtual线程(Virtual线程永不会被池化)并运行任务。

然后在范围上调用join方法,等待两个任务都完成或如果一个任务失败。更重要的——若一个任务失败,join()方法将自动向其他任务(剩余运行任务)发送取消请求并等待其终止。这很重要,因为取消请求将确保在块退出时没有不必要的悬挂任务。

若其他线程向父线程发取消请求,也是如此。在最后,若块内部任何位置抛异常——StructuredTaskScope的close方法将确保向子任务发送取消请求并终止任务。StructuredTaskScope美妙在于——若子线程创建自己的StructuredTaskScope(子任务本身有自己的子任务),取消时它们都会得到干净处理。

开发在这里的一个职责是确保它们编写的任务须正确处理在取消期间设置在线程上的中断标志。任务有责任读取此中断标志并干净终止自己。若任务未正确处理中断标志,那用例的响应性将受影响。

6 使用StructuredTaskScope

当一个用例需要将任务分解为子任务,可能还需将子任务进一步分解为更多子任务时,使用StructuredTaskScope是合适的。本文看到的示例是用例需在任一子任务失败时立即返回。但StructuredTaskScope远不止如此。

  • 在第一个任务成功时返回
  • 在所有任务完成时返回(成功或失败)
  • 制作自己的StructuredTaskScope版本

6.1 StructuredTaskScope优点

  • 代码易阅读,因为无论哪种用例,代码看着都一样
  • 子线程失败时会在适当时被干净终止。没有不必要的悬挂线程
  • 使用StructuredTaskScope与Virtual Threads一起,意味与阻塞相关可扩展性问题不存在。这也难怪,默认情况下,StructuredTaskScope在底层使用Virtual Threads

7 总结

总的来说,StructuredTaskScope类是Java中处理将任务拆分为多个子任务的用例的良好补充。子线程在失败时自动取消,不同用例的代码一致性以及更好地理解代码的能力,使其成为在Java中实现Structured Concurrency的理想选择。

Virtual Threads和StructuredTaskScope类共同组成了一个完美的组合。Virtual Threads使我们能够在JVM中创建数十万个线程,而StructuredTaskScope类使我们能够有效地管理这些线程。

让我们等待它退出预览并成为一个正式特性!

本文由博客一文多发平台 OpenWrite 发布!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/211803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity中Shader黑白阀值后处理效果

文章目录 前言一、我们先来PS看一下黑白阀值的效果二、使用step(a,b)函数实现效果三、实现脚本控制黑白阀值1、在Shader属性面板定义控制阀值变量2、把step的a改为_Value3、在后处理脚本设置公共成员变量,并且设置范围为&#xff08;0&#xff0c;1&#xff09;4、在Graphics.B…

Cocos Creator:创建棋盘

Cocos Creator&#xff1a;创建棋盘 创建地图三部曲&#xff1a;1. 创建layout组件2. 创建预制体Prefab&#xff0c;做好精灵贴图&#xff1a;3. 创建脚本LayoutSprite.ts收尾工作&#xff1a; 创建地图三部曲&#xff1a; 1. 创建layout组件 使用layout进行布局&#xff0c;…

四十三、Redis基础

目录 一、认识NoSql 1、定义&#xff1a; 2、常见语法 3、与关系型数据库&#xff08;SQL&#xff09;的区别&#xff1a; 二、认识Redis 1、定义&#xff1a; 2、特征&#xff1a; 3、Key的结构&#xff1a; 三、安装Redis 四、Redis常见命令 1、数据结构介绍 2、…

关于DNS服务器地址总是127.0.0.1且无法解析域名地址

问题 笔者尝试nslookup解释域名时&#xff0c;出现服务器变成本地环回口地址&#xff0c;导致无法解析域名 C:\Users\Zsy>nslookup www.baidu.com 服务器: UnKnown Address: 127.0.0.1*** UnKnown 找不到 www.baidu.com: Server failed排查思路 尝试关闭虚拟网卡&#…

CSS的逻辑组合伪类

CSS 的逻辑组合伪类有 4 种&#xff0c;分别是&#xff1a;:not()、:is()、:where()和&#xff1a;has()。 否定伪类:not() :not 伪类选择器用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中&#xff0c;它也被称为反选伪类&#xff08;negation pseudo-…

自动化测试框架 —— pytest框架入门篇

今天就给大家说一说pytest框架。 今天这篇文章呢&#xff0c;会从以下几个方面来介绍&#xff1a; 01、pytest框架介绍 pytest 是 python 的第三方单元测试框架&#xff0c;比自带 unittest 更简洁和高效&#xff0c;支持非常丰富的插件&#xff0c;同时兼容 unittest 框架。…

【C++】:AVL树

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关多态的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结…

用python 网络自动化统计交换机有多少端口UP

用python统计交换机有多少端口UP 用python统计交换机有多少端口UP&#xff0c;可以间接的反馈有多少个用户在线。我们使用上次的脚本将可达的网络设备ip统计到reachable_ip.txt中&#xff0c;这次我们使用reachable_ip.txt来登陆设备来统计多少端口是UP的 云配置 拓扑 交换机…

【Cisco Packet Tracer】VLAN通信 多臂/单臂路由/三层交换机

在进行本文的实验之前&#xff0c;请确保掌握以下内容&#xff1a; 【Cisco Packet Tracer】交换机 学习/更新/泛洪/VLAN实验 【Cisco Packet Tracer】路由器实验 静态路由/RIP/OSPF/BGP 【Cisco Packet Tracer】路由器 NAT实验 本文介绍VLAN间的通信方法&#xff0c; 包括…

FreeRTOS的任务优先级、Tick以及状态讲解(尊敬的嵌入式工程师,不妨进来喝杯茶)

任务优先级和Tick 在FreeRTOS中&#xff0c;任务的优先级和Tick是两个关键的概念&#xff0c;它们直接影响任务的调度和执行。 任务优先级 每个任务都被分配一个优先级&#xff0c;用于决定任务在系统中的调度顺序。 优先级是一个无符号整数&#xff0c;通常从0开始&#xff0…

Mysql- 流程函数-(If, CASE WHEN)的使用及练习

目录 4.1 If函数语法格式 4.2 CASE WHEN 条件表达式格式 4.3 update与 case when 4.4 练习题1 4.5 练习题2 4.6 练习题3-行转列 4.7 牛客练习题 4.8 LeetCode练习题 4.1 If函数语法格式 IF(expr1,expr2,expr3) 解释&#xff1a; 如果表达式expr1true(expr1 <>…

Xcode doesn’t support iOS 16.6

xocde版本低&#xff0c;手动放入16.6的依赖文件 https://gitee.com/qiu1993/iOSDeviceSupport/blob/master/iOS16/16.6.zip 路径 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport

分割回文串

分割回文串 描述 : 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 题目 : LeetCode 131.分割回文串 : 131. 分割回文串 分析 : 字符串如何判断回文本…

20 Redis进阶 - 运维监控

1、理解Redis监控 Redis运维和监控的意义不言而喻&#xff0c;可以以下三个方面入手 1.首先是Redis自身提供了哪些状态信息&#xff0c;以及有哪些常见的命令可以获取Redis的监控信息; 2.一些常见的UI工具可以可视化的监控Redis; 3.理解Redis的监控体系;2、Redis自身状态及命…

Vue3-02-ref() 响应式详解

ref() 是什么 ref() 是一个函数&#xff1b; ref() 函数用来声明响应式的状态&#xff08;就是来声明变量的&#xff09; ref() 函数声明的变量&#xff0c;是响应式的&#xff0c;变量的值改变之后&#xff0c;页面中会自动重新渲染。ref() 有什么特点 1.ref() 可以声明基础…

VUE语法--toRefs与toRef用法

1、功能概述 ref和reactive能够定义响应式的数据&#xff0c;当我们通过reactive定义了一个对象或者数组数据的时候&#xff0c;如果我们只希望这个对象或者数组中指定的数据响应&#xff0c;其他的不响应。这个时候我们就可以使用toRefs和toRef实现局部数据的响应。 toRefs是…

MIT线性代数笔记-第28讲-正定矩阵,最小值

目录 28.正定矩阵&#xff0c;最小值打赏 28.正定矩阵&#xff0c;最小值 首先正定矩阵是一个实对称矩阵 由第 26 26 26讲的末尾可知正定矩阵有以下四种判定条件&#xff1a; 所有特征值都为正左上角所有 k k k阶子矩阵行列式都为正&#xff08; 1 ≤ k ≤ n 1 \le k \le n …

DDD系列 - 第6讲 仓库Repository及Mybatis、JPA的取舍(一)

目录 一、领域层定义仓库接口1.1 设计聚合1.2 定义仓库Repository接口二 、基础设施层实现仓库接口2.1 设计数据库2.2 集成Mybatis2.3 引入Convetor2.4 实现仓库三、回顾一、领域层定义仓库接口 书接上回,之前通过一个关于拆解、微服务、面向对象的故事,向大家介绍了如何从微…

58.Nacos源码分析2

三、服务心跳。 3.服务心跳 Nacos的实例分为临时实例和永久实例两种&#xff0c;可以通过在yaml 文件配置&#xff1a; spring:application:name: order-servicecloud:nacos:discovery:ephemeral: false # 设置实例为永久实例。true&#xff1a;临时; false&#xff1a;永久ser…

【C/C++笔试练习】多态的概念、虚函数的概念、虚表地址、派生类的虚函数、虚函数的访问、指针引用、动态多态、完全数计算、扑克牌大小

文章目录 C/C笔试练习选择部分&#xff08;1&#xff09;多态的概念&#xff08;2&#xff09;虚函数的概念&#xff08;3&#xff09;虚表地址&#xff08;4&#xff09;派生类的虚函数&#xff08;5&#xff09;虚函数的访问&#xff08;6&#xff09;分析程序&#xff08;7&…