首页 > 编程语言 >优雅处理并发:Java CompletableFuture最佳实践

优雅处理并发:Java CompletableFuture最佳实践

时间:2024-01-10 22:33:59浏览次数:35  
标签:异步 return 处理 并发 CompletableFuture 操作 Java supplyAsync

7T7AlJ.png

第1章:引言

大家好,我是小黑,今天,小黑要和大家聊聊CompletableFuture,这个Java 8引入的强大工具。

在Java传统的Future模式里,咱们都知道,一旦开始了一个异步操作,就只能等它结束,无法知道执行情况,也不能手动完成或者取消。而CompletableFuture呢,就像它的名字一样,是可以"完全控制"的Future。它提供了更多的控制,比如可以手动完成,可以处理异常,还可以把多个Future组合起来,进行更复杂的异步逻辑处理。

对于现代Java程序员来说,掌握CompletableFuture是必不可少的。无论是提高程序的响应性能,还是编写更加清晰、更具可读性的代码,它都能大显身手。

第2章:基本概念解读

那么,CompletableFuture到底是什么呢?简单来说,它是一种异步编程工具,可以帮助咱们在未来的某个时刻完成一个计算结果。与Future最大的不同是,它可以被显式地完成,意味着咱们可以在任何时候设置它的值。

让我们来看一个简单的例子。假设小黑要从网上查询某个产品的价格,这是一个耗时的操作,使用CompletableFuture,咱们就可以异步地完成这个任务:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        // 创建一个CompletableFuture实例
        CompletableFuture<String> futurePrice = CompletableFuture.supplyAsync(() -> {
            // 模拟耗时操作,比如调用外部API
            simulateDelay();
            return "100元";
        });

        // 在这里,咱们可以做一些其他的事情,不必等待价格查询的结果
        doSomethingElse();

        // 当结果准备好后,获取它
        String price = futurePrice.join();
        System.out.println("价格是:" + price);
    }

    private static void simulateDelay() {
        try {
            Thread.sleep(1000); // 模拟1秒的延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private static void doSomethingElse() {
        // 做一些其他的事情
        System.out.println("小黑在做其他的事情...");
    }
}

在这个例子中,supplyAsync方法创建了一个异步操作,模拟了一个耗时的价格查询过程。在查询价格的同时,主线程可以继续执行其他任务,比如doSomethingElse方法里的内容。当价格查询完成后,可以使用join方法来获取结果。这样的处理方式,让整个程序的执行效率大大提升,而且代码也更简洁明了。

CompletableFuture的美在于,它提供了一种新的编程范式,让咱们能够以声明式的方式描述复杂的异步逻辑。从上面的例子可以看出,CompletableFuture不仅让代码更加简洁,还让逻辑更加清晰,易于理解和维护。

第3章:创建CompletableFuture

1. 使用supplyAsync

最常见的创建方式是使用CompletableFuture.supplyAsync()。这个方法需要一个Supplier函数接口,通常用于执行异步计算。来看看小黑怎么用:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 模拟耗时的计算
    simulateTask("数据加载中");
    return "结果";
});

这个例子中,simulateTask模拟了一个耗时操作,比如从数据库加载数据。使用supplyAsync,咱们就能在另一个线程中执行这个任务,而主线程可以继续做其他事情。

2. 使用runAsync

如果咱们不关心异步任务的结果,只想执行一个异步操作,那就可以用runAsync。它接受一个Runnable函数接口,不返回任何结果:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    simulateTask("正在执行一些处理");
});

在这个例子里,simulateTask只是执行了一些操作,比如记录日志或者发送通知,但不返回任何内容。

3. 手动完成

有时候,咱们可能需要手动完成一个Future。比如,基于某些条件判断,决定是否提前返回结果。这时候可以用complete方法:

CompletableFuture<String> manualFuture = new CompletableFuture<>();
// 在某些条件下手动完成Future
if (checkCondition()) {
    manualFuture.complete("手动结果");
}

如果checkCondition返回true,那么这个Future就会被立即完成,否则它将保持未完成状态。

4. 组合使用

CompletableFuture真正的魅力在于它的组合能力。假设小黑有两个独立的异步任务,咱们可以这样组合它们:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    simulateTask("加载用户数据");
    return "用户小黑";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    simulateTask("加载配置信息");
    return "配置信息";
});

// 组合两个future,等待它们都完成
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (user, config) -> {
    return "处理结果: " + user + "," + config;
});

在这个例子中,thenCombine用于组合future1future2的结果。只有当这两个Future都完成时,才会调用thenCombine里的函数。

第4章:异步操作和链式调用

异步操作的力量

异步操作是指在一个线程中启动一个任务,让它在另一个线程中运行,从而不阻塞当前线程的执行。这在处理耗时任务时特别有用。举个例子,假设咱们要查询数据库,然后处理查询结果。如果同步执行,整个程序都得等着数据库查询完成,这就浪费了宝贵的时间。但如果用CompletableFuture实现异步,就可以在查询数据库的同时做其他事情。

链式调用的魅力

链式调用则是指一系列操作依次执行,前一个操作的结果作为下一个操作的输入。CompletableFuture支持多种链式调用方法,比如thenApply, thenAcceptthenRun

  • thenApply用于处理和转换CompletableFuture的结果。
  • thenAccept用于消费CompletableFuture的结果,不返回新的CompletableFuture。
  • thenRun则不关心前一个任务的结果,只是在前一个任务执行完后,执行一些后续操作。

来看看小黑准备的例子:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    simulateTask("查询数据库");
    return "查询结果";
});

future.thenApply(result -> {
    // 对结果进行处理
    return "处理后的结果:" + result;
}).thenAccept(processedResult -> {
    // 消费处理后的结果
    System.out.println("最终结果:" + processedResult);
}).thenRun(() -> {
    // 执行一些不需要前一个结果的操作
    System.out.println("所有操作完成");
});

在这个例子里,小黑用supplyAsync启动了一个异步任务来查询数据库。然后用thenApply处理查询结果,用thenAccept消费处理后的结果,最后用thenRun标记所有操作完成。

通过这种方式,咱们可以构建出复杂的异步逻辑,而代码却依然保持清晰和易于管理。这就是CompletableFuture的魅力所在。

第5章:异常处理

基本异常处理

在CompletableFuture的世界里,如果异步操作失败了,异常会被捕获并存储在Future对象中。咱们可以使用exceptionally方法来处理这些异常。这个方法会返回一个新的CompletableFuture,它会在原来的Future抛出异常时执行。

来看个例子:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (new Random().nextBoolean()) {
        throw new RuntimeException("出错啦!");
    }
    return "正常结果";
}).exceptionally(ex -> {
    return "错误的回退结果:" + ex.getMessage();
});

future.thenAccept(System.out::println);

这里,小黑创建了一个可能会失败的异步操作。如果抛出异常,exceptionally方法就会被调用,返回一个包含错误信息的回退结果。

细粒度的异常处理

有时候,咱们可能需要更细粒度的控制,比如只处理特定类型的异常,或者在异常发生时还想继续其他操作。这时候,可以用handle方法。它可以同时处理正常的结果和异常情况。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (new Random().nextBoolean()) {
        throw new RuntimeException("出错啦!");
    }
    return "正常结果";
}).handle((result, ex) -> {
    if (ex != null) {
        return "处理异常:" + ex.getMessage();
    }
    return "处理结果:" + result;
});

future.thenAccept(System.out::println);

在这个例子中,无论异步操作是成功还是失败,handle方法都会被调用。如果有异常,它会处理异常;如果没有,就处理正常结果。

管道式异常处理

CompletableFuture还允许咱们创建一个异常处理的“管道”,这样就可以把多个异步操作链接起来,并在链的任意位置处理异常。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 第一个异步操作
    return "第一步结果";
}).thenApply(result -> {
    // 第二个异步操作,可能会出错
    throw new RuntimeException("第二步出错啦!");
}).exceptionally(ex -> {
    // 处理异常
    return "在第二步捕获异常:" + ex.getMessage();
}).thenApply(result -> {
    // 第三个异步操作
    return "第三步使用结果:" + result;
});

future.thenAccept(System.out::println);

在这个例子中,小黑创建了一个包含三个步骤的异步操作链。如果第二步出错,异常会被捕获并处理,然后处理结果被传递到第三步。

第6章:组合与依赖

组合多个Future

最常用的方法之一是thenCombine。这个方法允许你组合两个独立的CompletableFuture,并且当它们都完成时,可以对它们的结果进行一些操作。

来看个例子:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    simulateTask("加载用户信息");
    return "用户小黑";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    simulateTask("加载订单数据");
    return "订单123";
});

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (userInfo, orderInfo) -> {
    return "合并结果:" + userInfo + "," + orderInfo;
});

combinedFuture.thenAccept(System.out::println);

在这个例子中,future1future2代表两个独立的异步操作。只有当两者都完成时,thenCombine里面的函数才会执行,并且合并它们的结果。

依赖关系的处理

如果你的一个异步操作依赖于另一个异步操作的结果,那么可以使用thenCompose方法。这个方法允许你在一个Future完成后,以其结果为基础启动另一个异步操作。

CompletableFuture<String> masterFuture = CompletableFuture.supplyAsync(() -> {
    simulateTask("获取主数据");
    return "主数据结果";
});

CompletableFuture<String> dependentFuture = masterFuture.thenCompose(result -> {
    return CompletableFuture.supplyAsync(() -> {
        simulateTask("处理依赖于" + result + "的数据");
        return "处理后的数据";
    });
});

dependentFuture.thenAccept(System.out::println);

这个例子中,dependentFuture的执行依赖于masterFuture的结果。

处理多个Future

有时候,咱们可能有多个异步操作,需要等所有操作都完成后再进行下一步。这时候,可以使用CompletableFuture.allOf

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    simulateTask("任务一");
    return "结果一";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    simulateTask("任务二");
    return "结果二";
});

CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);

allFutures.thenRun(() -> {
    System.out.println("所有任务完成");
});

allOf会等待所有提供的Futures完成,然后执行后续操作。

第7章:最佳实践

1. 明智地选择异步任务执行方式

CompletableFuture提供了多种执行异步任务的方法,比如runAsyncsupplyAsync。默认情况下,它们使用公共的ForkJoinPool,但在某些场景下,你可能想要使用自定义的线程池来更好地控制资源。

ExecutorService customExecutor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "使用自定义线程池";
}, customExecutor);

这样做可以让你更好地管理线程资源,尤其是在处理大量异步任务时。

2. 谨慎处理阻塞操作

如果你的CompletableFuture链中包含阻塞调用,如数据库操作或文件I/O,最好是将这些操作放在独立的线程池中,避免阻塞ForkJoinPool中的线程。

ExecutorService dbExecutor = Executors.newCachedThreadPool();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // 这里是阻塞的数据库操作
    simulateTask("数据库操作");
}, dbExecutor);

这样可以防止长时间的阻塞操作占用过多的计算资源,影响整体性能。

3. 组合异步操作时的错误处理

当你组合多个CompletableFuture时,记得对每一个Future都进行错误处理。这样可以避免一个未捕获的异常破坏整个操作链。

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1").exceptionally(ex -> "默认值1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2").exceptionally(ex -> "默认值2");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " 和 " + result2);

这样做确保了即使其中一个操作失败,整个链也可以继续执行。

4. 避免过多的链式调用

虽然链式调用是CompletableFuture的一个强大特性,但过度使用可能会导致代码难以理解和维护。建议把复杂的逻辑分解成多个方法或类。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "原始数据")
    .thenApply(this::step1)
    .thenApply(this::step2)
    .thenApply(this::step3);

// 将每个步骤的逻辑封装在不同的方法中
private String step1(String data) {
    return "处理1:" + data;
}

private String step2(String data) {
    return "处理2:" + data;
}

private String step3(String data) {
    return "处理3:" + data;
}

第8章:总结

  1. 异步编程的强大工具:CompletableFuture为Java异步编程提供了强大的支持,让处理并发任务变得更简单、更灵活。

  2. 简化复杂逻辑:通过链式调用和组合多个异步任务,CompletableFuture能够帮助咱们以清晰的方式处理复杂的业务逻辑。

  3. 异常处理的优雅方式:CompletableFuture提供了一套完整的异常处理框架,让咱们能够更好地控制和管理异步代码中的错误情况。

标签:异步,return,处理,并发,CompletableFuture,操作,Java,supplyAsync
From: https://blog.51cto.com/u_16326109/9186652

相关文章

  • 《实战Java虚拟机 JVM故障诊断与性能优化》读书
    最近读了《实战Java虚拟机JVM故障诊断与性能优化》这本书,有一些收获,特此记录1.有Java虚拟机规范,各大厂商根据规范自己实现VM,大家平常用的是Hotspot2.堆一般分为新生代,老年代。新生代里又分成伊甸园区,from区,to区。一般对象是在伊甸园区出生,经过垃圾回收进入fromto老年代3.J......
  • java.lang.RuntimeException: setParameters failed 解读
    解决java.lang.RuntimeException:setParametersfailed错误在Java开发中,当我们使用相机(Camera)功能进行拍照或录像时,有时可能会遇到java.lang.RuntimeException:setParametersfailed这样的错误。这个错误通常表示相机参数设置失败,导致无法进行预期的相机操作。本篇文章将介绍可......
  • Javassist实例
    Javassist强调源代码层次操作字节码的框架。利用Javassist实现字节码增强时,可以无须关注字节码的结构,其优点在于编简单。直接使用Java编程的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成的类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:CtClass......
  • Java字节码文件查看常用工具​
    04.字节码常用工具javap-v命令javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。直接输入javap查看所有参数。输入javap-v字节码文件名称查看具体的字节码信息。(如果是jar包需要先使用jar-xvf命令解压)jclasslib的idea插件j......
  • 【从零开始重学Java】第11天
    前情提示从零开始重学Java第0天从零开始重学Java第1天从零开始重学Java第2天从零开始重学Java第3天从零开始重学Java第4天从零开始重学Java第5天从零开始重学Java第6天从零开始重学Java第7天从零开始重学Java第8_9_10天Java数据流关于文件的操作建议使用新的......
  • 高并发扣款,如何保证结果一致性
    转载至我的博客,公众号:架构成长指南在金融系统中,我们会跟钱打交道,而保证在高并发下场景下,对账户余额操作的一致性,是非常重要的,如果代码写的时候没考虑并发一致性,就会导致资损,本人在金融行业干了8年多,对这块稍微有点经验,所以这篇聊一下,如何在并发场景下,保证账户余额的一致性1......
  • Java学习积累
    Java学习积累对象判空在判断获取的信息是否为空时需要注意字段类型的区别如果是String类型变量需要采用string==nullStrUtil.isEmpty(string)StringUtils.isEmpty一种是JDKString包中的内容一种是Springboot中的内容(可能5.3版本之后这个方法就没有用了)在判断......
  • 2024年1月Java项目开发指南1:环境与工具准备
    准备工作基础能力开发能力的事咱先不谈,有两个基础技能要学一下。1.学习使用Markdown编写文档2.学会使用git拉取代码和提交代码软件准备电脑需要安装以下软件:IDEA2023.2及其以上(Java开发统一使用IDEA)Webstorm2023.2及其以上(也可以使用VScode或者Hbuilder或者Dreamweav......
  • 2024年1月Java项目开发指南2:项目设计
    确定软件架构1.前端用什么技术,什么框架,什么版本2.后端用什么技术,什么框架,什么版本3.用些什么软件,软件版本?比如:前端:vue3+Element-Plus+Axios后端:Java17SpringbootMyBatis-plusMaven3数据库:MySQL5.5后端开发工具:IDEA2023.2.2前端开发工具:Webstorm2023.2.2接口管理......
  • 【JavaScript】JavaScript定义、引入方式、基础语法、函数、对象、继承
    定义JavaScript是一门跨平台、面向对象的脚本语言用来控制网页行为的,可以使网页交互引入方式内部脚本外部脚本JS基础语法书写语法输出语句变量变量作用域数据类型运算符=====??.?...展开运算符(1)打散数组传递给多个参数(2)复制数组或......