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

news2024/11/29 22:41:29

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任务提交给表示单线程线程池的ExecutorService

ExecutorService 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、result3
    handleResults(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实现Supplier
     var 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.coloradmin.cn/o/1299832.html

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

相关文章

【图片版】计算机组成原理考前复习题【第2章 运算方法和运算器-1】

目录 前言 考前复习题&#xff08;必记&#xff09; 结尾 前言 在计算机组成原理的学习过程中&#xff0c;我们深入探索了计算机系统概述这一重要领域。计算机系统作为现代科技的核心&#xff0c;是整个计算机科学的基石。我们将学到的知识与理论转化为了能够解决现实问题的…

3DMAX关于显示驱动问题的解决方法大全

3DMAX与显卡驱动有关的问题主要有以下几种情况&#xff1a; 1.3DMAX启动弹出这样的界面&#xff1a; 2.主工具栏按钮不显示&#xff0c;或者鼠标移上去才显示&#xff08;刷新问题&#xff09;。 3&#xff0e;视口菜单不显示或显示不全。 问题分析&#xff1a; 首先&#x…

AspNetCore 中使用 Knife4jUI 更加友好的Swagger界面

&#x1f680;介绍 aspnetcore.knife4j是一个基于.NET Core平台的Swagger UI库&#xff0c;它提供了API文档的生成和管理功能。这个库的前身是swagger-bootstrap-ui&#xff0c;在Java项目中广泛使用&#xff0c;由于其优秀的界面和易用性被许多开发者所推崇。现在&#xff0c…

一文学会使用 PyInstaller 将 Python 脚本打包为 .exe 可执行文件

文章目录 前言PyInstaller特点跨平台支持自动依赖项处理单文件发布支持图形用户界面&#xff08;GUI&#xff09;和命令行界面&#xff08;CLI&#xff09;应用支持多种打包选项 基本用法常用参数其它参数 版本 & 环境实现步骤安装 PyInstaller创建 Python 脚本使用 PyInst…

C++学习笔记之五(String类)

C 前言getlinelength, sizec_strappend, inserterasefindsubstrisspace, isdigit 前言 C是兼容C语言的&#xff0c;所以C的字符串自然继承C语言的一切字符串&#xff0c;但它也衍生出属于自己的字符串类&#xff0c;即String类。String更像是一个容器&#xff0c;但它与容器还…

HJ103 Redraiment的走法

题目&#xff1a; HJ103 Redraiment的走法 题解&#xff1a; dfs 暴力搜索 枚举数组元素&#xff0c;作为起点如果后续节点大于当前节点&#xff0c;继续向后搜索记录每个起点的结果&#xff0c;求出最大值 public int getLongestSub(int[] arr) {int max 0;for (int i 0…

MySQL - 表达式With as 语句的使用及练习

目录 8.1 WITH AS 的含义 8.2 WITH AS语法的基本结构如下&#xff1a; 8.3 练习题1 8.4 牛客练习题 8.1 WITH AS 的含义 WITH AS 语法是MySQL中的一种临时结果集&#xff0c;它可以在SELECT、INSERT、UPDATE或DELETE语句中使用。通过使用WITH AS语句&#xff0c;可以将一个查…

flstudio21.3完整高级版怎么下载?有哪些新功能

flstudio高级版是一款适用于广泛领域的音频编辑软件。它支持多通道混音器和VST插件&#xff0c;包括数百种乐器和效果插件。它还为您提供了一个乐谱编辑器&#xff0c;需要对不同乐器的节奏进行必要的编辑。Flstudio具有许多内置电子合成声音&#xff0c;可提供更广泛的电子声音…

运维06:监控

监控生命周期 1.服务器上架到机柜2.基础设施监控 服务器温度&#xff0c;风扇转速 ipmitool命令&#xff0c;只能用在物理机上 存储的监控&#xff08;df, fdisk, iotop&#xff09; cpu&#xff08;lscpu, uptime, top, htop, glances&#xff09; 内存情况&#xff08;free&…

CSS-自适应导航栏(flex | grid)

目标&#xff1a;实现左右各有按钮&#xff0c;中间是内容&#xff0c;自适应显示中间的内容导航栏&#xff0c;即 根据中间的宽度大小显示内容。 自适应导航栏 总结&#xff1a;推荐 flex布局 / grid布局 flex布局&#xff1a; 两侧 flex:1; ----->中间自适应 grid布局&…

【MATLAB】基于EEMD分解的信号去噪算法(基础版)

代码操作 【MATLAB】基于EEMD分解的信号去噪算法&#xff08;基础版&#xff09; 代码的主要内容 基于EEMD&#xff08;集合经验模态分解&#xff09;的信号去噪算法通常可以结合相关系数、信号的熵值或者方差贡献率来完成去噪处理。这些指标可以用于确定阈值&#xff0c;从而…

Java线程概念详解

线程 概念 1.程序:未解决某种问题,使用计算机语言编写的一些列指令(代码)的集合 2.进程:正在运行的程序(被加载到内存中),是操作系统进行资源分配的最小单位 3.线程:进程可以进一步细化为线程(比进程更小)且线程是隶属于进程的,是操作系统执行的最小的执行单元 也是cpu进行任…

整合,降价,官司……2023休闲零食行业大变局

休闲零食行业正从暗战走向明战。 11月27日&#xff0c;宁波广源聚亿投资有限公司&#xff08;以下简称“广源聚亿”&#xff09;因被投企业宜春赵一鸣食品科技有限公司&#xff08;以下简称“赵一鸣”&#xff09;在双方合作期内&#xff0c;刻意隐瞒公司重大事项&#xff0c;…

Matlab数学建模算法之小波神经网络详解

&#x1f517; 运行环境&#xff1a;Matlab &#x1f6a9; 撰写作者&#xff1a;左手の明天 &#x1f947; 精选专栏&#xff1a;《python》 &#x1f525; 推荐专栏&#xff1a;《算法研究》 &#x1f510;#### 防伪水印——左手の明天 ####&#x1f510; &#x1f497; 大家…

PHP使用mkcert本地开发生成HTTPS证书 PhpEnv集成环境

PHP使用mkcert本地开发生成HTTPS证书 PhpEnv集成环境 前言一、介绍 mkcert二、安装/使用 mkcert1. 安装2. 使用 总结 前言 本地开发时有些功能只有在 https 证书的情况下才能使用, 例如一些 Web API 一、介绍 mkcert Github地址 mkcert 是一个制作本地可信开发证书的简单工具。…

基于以太坊的智能合约开发Solidity(事件日志篇)

//声明版本号&#xff08;程序中的版本号要和编译器版本号一致&#xff09; pragma solidity ^0.5.17; //合约 contract EventTest {//状态变量uint public Variable;//构造函数constructor() public{Variable 100;}event ValueChanged(uint newValue); //事件声明event Log(…

无人零售店,凭借黑科技引领,它的前景如何?

无人零售店&#xff0c;凭借黑科技引领&#xff0c;它的前景如何&#xff1f; 近年来&#xff0c;无人新零售行业快速兴起&#xff0c;引起了广泛的关注和兴趣。尤其是在疫情之后&#xff0c;人们才真正认识到了无人零售的概念。如今&#xff0c;无人零售行业正在以直线的方式高…

U2 文法和语言

文章目录 一、预备知识1、字母表和符号串2、符号串形式定义3、符号串相等4、延申 二、文法1、 一些概念2、文法的定义3、推导的形式定义4、规范推导5、语言的形式定义6、递归文法7、BNF范式总结 三、短语、简单短语和句柄四、语法树(推导树)1、概念2、语法树推导--句型的推导3、…

打造Github首页的动态飞线效果

一、导语 Github首页的地球动态飞线&#xff0c;大家都比较熟悉吧 二、分析 由大量随机的3点构造出贝塞尔曲线&#xff0c;然后开始从起点到终点的飞行后&#xff0c;然后再从起点到终点的消失&#xff0c;就此完成整个过程 三、基础代码 createCurve(startPoint, endPoint…

爬虫解析-jsonpath (六)

jsonpath只能解析本地文件 jsonpath的使用&#xff1a; obj json.load(open(.json文件,r,encodingutf-8))place_name jsonpath.jsonpath(obj, json语法) 目录 1.安装jsonpath 2.Xpath和jsonpath的语法对比 练习&#xff1a;使用jsonpath解析JSON文件 3.使用jsonpath抓取…