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

本文详细介绍了Java中异步编程的关键工具CompletableFuture,包括Future接口的局限性、CompletableFuture的创建和常用API、异步任务链、异常处理、以及它在实际应用如RocketMQ中的使用。CompletableFuture因其灵活性、可读性和强大功能在现代应用程序开发中占据重要地位。
摘要由CSDN通过智能技术生成

在现代应用程序开发中,异步编程已经成为不可或缺的一部分。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(() -> {
    // 模拟耗时任务1
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Result from Task 1";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    // 模拟耗时任务2
    try {
        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 开发中不可或缺的一部分。

  • 31
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据你提供的信息,我猜测你的代码大致如下: ```java CompletableFuture<Integer>[] futuresDa = new CompletableFuture[floorIds.size()]; CompletableFuture<List<Map<String,Object>>>[] futuresXiao = new CompletableFuture[floorIds.size()]; for (int i = 0; i < floorIds.size(); i++) { CompletableFuture<List<Map<String,Object>>> addDaXiang = addDaXiang(floorIds.get(i)); CompletableFuture<List<Map<String,Object>>> addXiaoXiang = addXiaoXiang(floorIds.get(i)); futuresDa[i] = addDaXiang.thenApplyAsync(daXiang -> { // some code here return someValue; }); futuresXiao[i] = addXiaoXiang.thenApplyAsync(xiaoXiang -> { // some code here return someValue; }); } CompletableFuture.allOf(futuresDa).join(); ``` 根据代码分析,我认为你出现空指针异常的原因是因为`addDaXiang`方法返回的`CompletableFuture`对象为空。这可能是因为在异步方法内部出现了异常,导致返回的`CompletableFuture`对象没有正确地被赋值。 你可以在`addDaXiang`方法内部添加异常处理逻辑,将异常信息打印出来,以便你更好地定位问题所在。例如: ```java public CompletableFuture<List<Map<String,Object>>> addDaXiang(int floorId) { CompletableFuture<List<Map<String,Object>>> future = new CompletableFuture<>(); // some code here try { // some code here future.complete(someValue); } catch (Exception e) { future.completeExceptionally(e); } return future; } ``` 这样,如果`addDaXiang`方法内部出现异常,将会把异常信息放入`CompletableFuture`对象中,并返回给调用者。在调用者处,你可以通过`CompletableFuture.exceptionally()`方法来处理异常信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值