Java 异步编程进阶:CompletableFuture 完全指南

在现代应用程序开发中,异步编程已经成为不可或缺的一部分。Java提供了许多用于异步编程的工具和框架,其中最强大的之一是 CompletableFuture。CompletableFuture 不仅简化了异步任务的管理,而且提供了丰富的 API,使得开发人员可以轻松地实现复杂的异步操作。本文将深入探讨 CompletableFuture 的使用方法和最佳实践,帮助您充分利用这一强大的工具。

一、Future接口以及它的局限性

我们都知道,Java中创建线程的方式主要有两种方式,继承Thread或者实现Runnable接口。但是这两种都是有一个共同的缺点,那就是都无法获取到线程执行的结果,也就是没有返回值。于是在JDK1.5 以后为了解决这种没有返回值的问题,提供了Callable和Future接口以及Future对应的实现类FutureTask,通过FutureTask的就可以获取到异步执行的结果。
于是乎,我们想要开启异步线程,执行任务,获取结果,就可以这么实现。

 FutureTask<String> futureTask = new FutureTask<>(() -> "三友");new Thread(futureTask).start();System.out.println(futureTask.get());

或者使用线程池的方式

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "三友");
System.out.println(future.get());
executorService.shutdown();

线程池底层也是将提交的Callable的实现先封装成FutureTask,然后通过execute方法来提交任务,执行异步逻辑。

二、Future接口的局限性

虽然通过Future接口的get方法可以获取任务异步执行的结果,但是get方法会阻塞主线程,也就是异步任务没有完成,主线程会一直阻塞,直到任务结束。
Future也提供了isDone方法来查看异步线程任务执行是否完成,如果完成,就可以获取任务的执行结果,代码如下。

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "三友");
while (!future.isDone()) {//任务有没有完成,没有就继续循环判断
}
System.out.println(future.get());
executorService.shutdown();

但是这种轮询查看异步线程任务执行状态,也是非常消耗cpu资源。
同时对于一些复杂的异步操作任务的处理,可能需要各种同步组件来一起完成。
所以,通过上面的介绍可以看出,Future在使用的过程中还是有很强的局限性,所以为了解决这种局限性,在JDK1.8的时候,Doug Lea 大神为我们提供了一种更为强大的类CompletableFuture。

三、CompletableFuture 简介

CompletableFuture 是 Java 8 引入的一个类,用于异步编程。它实现了 Future 接口,并添加了许多额外的方法,使得异步编程更加灵活和强大。与传统的 Future 相比,CompletableFuture 允许我们显式地完成一个异步任务,而不必等待其完成。

四、CompletableFuture常见api详解

1. 创建 CompletableFuture

首先,让我们看看如何创建一个 CompletableFuture 实例。CompletableFuture 提供了两个静态方法来执行异步任务:supplyAsync 和 runAsync。

1.1 supplyAsync 方法

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 执行异步任务,返回结果return "Hello, CompletableFuture!";
});

supplyAsync 方法接受一个 Supplier 接口作为参数,该接口代表一个有返回值的异步任务。

1.2 runAsync 方法

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {// 执行异步任务,无返回结果
});

runAsync 方法接受一个 Runnable 接口作为参数,该接口代表一个没有返回值的异步任务。

2. 异步任务链

CompletableFuture 提供了一系列方法来构建异步任务链,使得多个异步任务能够按照特定的顺序依次执行。

2.1 thenApply 方法

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 执行异步任务,返回结果return "Hello";
}).thenApply(result -> result + ", CompletableFuture!");

thenApply 方法接受一个 Function 接口作为参数,用于处理上一个任务的结果,并返回一个新的 CompletableFuture。

2.2 thenAccept 方法

CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {// 执行异步任务,返回结果return "Hello";
}).thenAccept(result -> {// 处理任务结果,无返回值System.out.println("Result: " + result);
});

thenAccept 方法接受一个 Consumer 接口作为参数,用于处理上一个任务的结果,但不返回任何结果。

2.3 thenCompose 方法

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello").thenCompose(result -> CompletableFuture.supplyAsync(() -> result + ", CompletableFuture!"));

thenCompose 方法接受一个 Function 接口作为参数,用于处理上一个任务的结果,并返回一个新的 CompletableFuture。与 thenApply 不同的是,thenCompose 返回的 CompletableFuture 是一个扁平化的结果,而不是嵌套的 CompletableFuture。

3. 异常处理

在异步编程中,异常处理是至关重要的一部分。CompletableFuture 提供了多种方法来处理异步任务中可能发生的异常。

3.1 exceptionally 方法

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 执行异步任务,可能抛出异常throw new RuntimeException("Oops! Something went wrong.");
}).exceptionally(ex -> {// 处理异常情况,并返回默认值System.out.println("Exception occurred: " + ex.getMessage());return "Default Value";
});

exceptionally 方法用于处理异常情况,并返回一个默认值或者处理结果。

3.2 handle 方法

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 执行异步任务,可能抛出异常throw new RuntimeException("Oops! Something went wrong.");
}).handle((result, ex) -> {// 处理任务结果或异常,并返回处理后的结果if (ex != null) {System.out.println("Exception occurred: " + ex.getMessage());return "Default Value";}return result;
});

handle 方法接受一个 BiFunction 接口作为参数,用于处理任务的结果或异常,并返回一个新的结果。

4. 组合多个 CompletableFuture

有时候,我们需要等待多个异步任务都完成后才执行下一步操作。CompletableFuture 提供了 allOf 和 anyOf 方法来实现这一目的。

4.1 allOf 方法

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "CompletableFuture");CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
allFutures.thenRun(() -> {// 所有任务完成后执行下一步操作
});

allOf 方法接受多个 CompletableFuture 作为参数,并返回一个新的 CompletableFuture,当所有的 CompletableFuture 都完成时,该 CompletableFuture 才会完成。

4.2 anyOf 方法

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {// 模拟耗时任务1try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return "Result from Task 1";
});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {// 模拟耗时任务2try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}return "Result from Task 2";
});CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(future1, future2);
anyFuture.thenAccept(result -> {// 输出第一个完成的任务结果System.out.println("First completed task result: " + result);
});

anyOf 方法接受多个 CompletableFuture 作为参数,并返回一个新的 CompletableFuture,当任意一个 CompletableFuture 完成时,该 CompletableFuture 就会完成。

当然,还有一些其它的api,可以自行查看

五、CompletableFuture在RocketMQ中的使用

CompletableFuture在RocketMQ中的使用场景比较多,这里我举一个消息存储的场景。
在RocketMQ中,Broker接收到生产者产生的消息的时候,会将消息持久化到磁盘和同步到从节点中。持久化到磁盘和消息同步到从节点是两个独立的任务,互不干扰,可以相互独立执行。当消息持久化到磁盘和同步到从节点中任务完成之后,需要统计整个存储消息消耗的时间,所以统计整个存储消息消耗的时间是依赖前面两个任务的完成。

实现代码如下
消息存储刷盘任务和主从复制任务:

PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
// 提交刷盘的请求
CompletableFuture<PutMessageStatus> flushResultFuture = submitFlushRequest(result, msg);
//提交主从复制的请求
CompletableFuture<PutMessageStatus> replicaResultFuture = submitReplicaRequest(result, msg);//刷盘 和 主从复制 两个异步任务通过thenCombine联合
return flushResultFuture.thenCombine(replicaResultFuture, (flushStatus, replicaStatus) -> {// 当两个刷盘和主从复制任务都完成的时候,就会回调// 如果刷盘没有成功,那么就将消息存储的状态设置为失败if (flushStatus != PutMessageStatus.PUT_OK) {putMessageResult.setPutMessageStatus(flushStatus);}// 如果主从复制没有成功,那么就将消息存储的状态设置为失败if (replicaStatus != PutMessageStatus.PUT_OK) {putMessageResult.setPutMessageStatus(replicaStatus);}// 最终返回消息存储的结果return putMessageResult;
});

对上面两个合并的任务执行结果通过thenAccept方法进行监听,统计消息存储的耗时:

//消息存储的开始时间
long beginTime = this.getSystemClock().now();
// 存储消息,然后返回 CompletableFuture,也就是上面一段代码得返回值‍
CompletableFuture<PutMessageResult> putResultFuture = this.commitLog.asyncPutMessage(msg);//监听消息存储的结果
putResultFuture.thenAccept((result) -> {// 消息存储完成之后会回调long elapsedTime = this.getSystemClock().now() - beginTime;if (elapsedTime > 500) {log.warn("putMessage not in lock elapsed time(ms)={}, bodyLength={}", elapsedTime, msg.getBody().length);}this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime);if (null == result || !result.isOk()) {this.storeStatsService.getPutMessageFailedTimes().add(1);}
});

六、CompletableFuture 的优势

当涉及异步编程时,CompletableFuture 显露出许多优点:

  1. 灵活性:CompletableFuture 提供了丰富的
    API,使得异步任务的管理和处理变得更加灵活和高效。它可以很容易地与现有的同步和异步 API
    集成,并允许开发人员轻松地构建复杂的异步任务链。
  2. 组合和链式调用:CompletableFuture 允许开发人员使用 thenApply、thenAccept、thenCompose
    等方法构建异步任务链,使得多个异步任务能够按照特定的顺序依次执行,从而实现更加复杂的业务逻辑。
  3. 异常处理:CompletableFuture 提供了 exceptionally、handle
    等方法来处理异步任务中可能发生的异常。这使得开发人员能够更加方便地捕获和处理异常情况,从而提高了代码的健壮性和可靠性。
  4. 组合多个 CompletableFuture:CompletableFuture 提供了 allOf 和 anyOf 方法来组合多个
    CompletableFuture,使得开发人员能够等待多个异步任务都完成后才执行下一步操作,或者在任意一个异步任务完成时立即执行下一步操作。
  5. 异步任务取消:CompletableFuture 允许开发人员取消正在执行的异步任务,从而节省系统资源并提高程序的响应速度。通过调用
    cancel 方法,可以立即中止异步任务的执行,并抛出 CancellationException 异常。
  6. 可读性和维护性:相较于传统的 Future 接口,CompletableFuture
    的代码更加简洁、清晰,并且更容易理解和维护。它提供了更加直观和流畅的 API,使得异步编程变得更加愉快和高效。

综上所述,CompletableFuture 作为 Java 异步编程的利器,具有灵活性高、功能强大、易于使用等诸多优点,可以极大地提高开发人员的工作效率和代码质量。因此,它已经成为现代 Java 开发中不可或缺的一部分。

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

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

相关文章

Linux常见指令解析

基础命令行 1、rm可以删除文件&#xff08;rm -d /path/to/directory或者rm -r /path/to/directory&#xff09; 2、ls是展开文件 在linux中&#xff0c;“ll”是“ls -l”命令的别名&#xff0c;ls命令用于显示指定工作目录下之内容&#xff0c;参数“-l”表示除文件名称外&…

STM32之不使用MicroLIB

一、microlib介绍 microlib 是缺省 C 库的备选库,功能上不具备某些 ISO C 特性。 microlib 进行了高度优化以使代码变得很小,功能比缺省 C 库少,用于必须在极少量内存环境下运行的深层嵌入式应用程序。 二、不使用microlib的原因 由于microlib不支持C++开发,因此在使用C…

Java中函数式编程2

Java中的函数参数 在Java中&#xff0c;函数参数有以下三种形式&#xff1a; lambda表达式。方法引用。匿名内部类。 函数参数无论怎么表示&#xff0c;其原则为&#xff1a;1. 参数列表和返回值类型 与 要表示的抽象函数的相同。2. 方法体内部如果要使用外部变量&#xff0c…

element plus el-date-picker type=“datetime“ 限制年月日 时分秒选择

如何限制el-date-picker组件的时分秒选中&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 文档 文档在这里&#xff1a;DateTimePicker 日期时间选择器 | Element Plus 它提供的disabled-date给我们来限制日期选择 nice&#xff01;&…

mysql面试题四(事务)

目录 1.什么是数据库的事务 1. 原子性&#xff08;Atomicity&#xff09; 2. 一致性&#xff08;Consistency&#xff09; 3. 隔离性&#xff08;Isolation&#xff09; 4. 持久性&#xff08;Durability&#xff09; 2.事务的并发问题 1. 脏读&#xff08;Dirty Read&am…

探讨并行速率的评估方法及实验方案

引言 基础概念 并行计算的类型&#xff08;数据并行、任务并行&#xff09; 加速比 并行效率 如何评估并行算法 Amdahl定律与Gustafson定律的介绍 工具与平台 CPU/GPU/TPU等硬件平台的选择 软件和编程框架&#xff08;如OpenMP, MPI, CUDA&#xff09; 实验案例 简单…

2024年3月洗衣机大家电线上电商(京东天猫淘宝)销量排行榜

鲸参谋监测的线上电商&#xff08;京东天猫淘宝&#xff09;平台3月份的洗衣机大家电销售数据已出炉&#xff01; 根据鲸参谋数据显示&#xff0c;今年3月份&#xff0c;线上电商平台洗衣机的销量累计约224万件&#xff0c;环比增长了29%&#xff0c;环比增长了约29%&#xff…

ubuntu在线安装mysql数据库

1、命令 在ubuntu上安装mysql数据库&#xff0c;通过命令行的方式在线安装。 命令如下&#xff1a; # 更新系统软件包列表 sudo apt update# 安装MySQL Server sudo apt install mysql-server# 安装完成后&#xff0c;启动MySQL服务 sudo systemctl start mysql# 设置MySQL服…

网络变压器在网络分析仪上能通过测试,装上设备后网速达不到呢?

Hqst华轩盛(石门盈盛)电子导读&#xff1a;今天和大家一起探讨网络变压器在网络分析仪上能通过测试&#xff0c;装上设备后网通设备网速达不到的可能原因及其处理方式 一、出现这种情况可能有以下原因&#xff1a; 1.1. 设备兼容性问题&#xff1a;设备其它元器件与 网络…

14、ESP32 经典 Bluetooth

ESP32 上的内置经典蓝牙相比低功耗蓝牙较为简单&#xff0c;可以和 Android 智能手机之间交换数据。下面是官方例程&#xff1a; #include <Arduino.h> #include "BluetoothSerial.h"// 检查蓝牙是否正确启用 #if !defined(CONFIG_BT_ENABLED) || !defined(CO…

MATLAB绘制复杂分段函数图像

MATLAB绘制复杂分段函数图像 clc;close all;clear all;warning off;%清除变量 rand(seed, 200); randn(seed, 200) % 定义 x 范围和分辨率 x linspace(-2, 2, 1000); % 初始化 y 数组 y zeros(size(x)); % 分段定义函数 y(x < 0) x(x < 0).^2; y(x > 0 …

一个例子搞懂模型训练和参数更新的过程

模型训练和更新参数的过程是机器学习中的核心。这个过程通常涉及多个步骤&#xff0c;包括前向传播、损失计算、反向传播和参数更新。下面我将通过一个简单的线性回归模型的例子来解释这些步骤&#xff1a; 线性回归模型示例 假设我们有一个简单的线性关系 y w x b ywxb yw…

使用Termux在Android设备上编译运行SpecCPU2006

Spec CPU 2006 的使用说明&#xff08;曲线救国版&#xff09; 因本部分实验用到的Spec CPU2006依赖于多个编译工具包&#xff0c;因此对源码的编译要在配置好环境的Linux设备上运行&#xff0c;根据实验发现&#xff0c;现有的环境&#xff08;包括adb和termux&#xff09;都不…

FreeRTOS之动态创建任务与删除任务

1.本文是利用FreeRTOS来动态创建任务和删除任务。主要是使用FreeRTOS的两个API函数&#xff1a;xTaskCreate()和vTaskDelete()。 任务1和任务2是让LED0、LED1闪烁。任务3是当按键按下时删除任务1。 使用动态创建任务时&#xff0c;需要动态的堆中申请任务所需的内存空间&…

Jmeter redis连接测试

Jmeter连接redis获取数据&#xff0c;一直连不上报错。最后只能通过java代码连接测试&#xff0c;最后只能自己动手。 import redis.clients.jedis.*;import java.io.IOException; import java.util.HashSet; import java.util.Set;/*** 单机版的Jedis连接池的用法*/ public c…

Flask实战

from flask import Flask appFlask(__name__)点击Flask同时点击键盘ctrl即可查看Flask的默认初始化函数 def __init__(self,import_name: str,static_url_path: str | None None,static_folder: str | os.PathLike[str] | None "static",static_host: str | None …

安装docker的PHP环境NLMP环境在国产deepin操作系统上

1: 先安装docker 安装完后执行,权限设置 sudo usermod -aG docker $USER或者sudo usermod -aG docker kentrl#添加当前用户到Docker用户组中 sudo newgrp docker#更新用户组数据,必须执行否则无效 sudo systemctl restart docker 先看目录结构: 2:按照目录结构挂载磁盘,…

JavaScript(五)-正则表达式

文章目录 正则表达式正则表达式的介绍语法元字符修饰符 正则表达式 正则表达式的介绍 什么是正则表达式 正则表达式&#xff08;Regular expression&#xff09;是用于匹配字符串中字符组合的模式&#xff0c;在JavaScript中&#xff0c;正则表达式也是对象通常用来查找、替…

UE5数字孪生系列笔记(四)

场景的切换 创建一个按钮的用户界面UMG 创建一个Actor&#xff0c;然后将此按钮UMG添加到组件Actor中 调节几个全屏的背景 运行结果 目标点切换功能制作 设置角色到这个按钮的位置效果 按钮被点击就进行跳转 多个地点的切换与旋转 将之前的目标点切换逻辑替换成旋转的逻…

驱动开发platform传地址,led点灯

除了platform传地址&#xff0c;其他的跟指定入口地址和指定出口地址没区别 platform和指定入口地址不能同时存在&#xff0c;一直报错模块初始化重定义&#xff0c;半个小时搞完程序没问题&#xff0c;这个重复定义因为代码太多没看懂错误&#xff0c;删了又加没试出来怎么改…