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

第1章:引言

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

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

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

第2章:基本概念解读

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

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

java 复制代码
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函数接口,通常用于执行异步计算。来看看小黑怎么用:

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

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

2. 使用runAsync

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

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

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

3. 手动完成

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

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

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

4. 组合使用

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

java 复制代码
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则不关心前一个任务的结果,只是在前一个任务执行完后,执行一些后续操作。

来看看小黑准备的例子:

java 复制代码
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抛出异常时执行。

来看个例子:

java 复制代码
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方法。它可以同时处理正常的结果和异常情况。

java 复制代码
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还允许咱们创建一个异常处理的"管道",这样就可以把多个异步操作链接起来,并在链的任意位置处理异常。

java 复制代码
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,并且当它们都完成时,可以对它们的结果进行一些操作。

来看个例子:

java 复制代码
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完成后,以其结果为基础启动另一个异步操作。

java 复制代码
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

java 复制代码
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,但在某些场景下,你可能想要使用自定义的线程池来更好地控制资源。

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

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

2. 谨慎处理阻塞操作

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

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

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

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

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

java 复制代码
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的一个强大特性,但过度使用可能会导致代码难以理解和维护。建议把复杂的逻辑分解成多个方法或类。

java 复制代码
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提供了一套完整的异常处理框架,让咱们能够更好地控制和管理异步代码中的错误情况。

相关推荐
你的人类朋友36 分钟前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧1 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧1 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
zizisuo1 小时前
解决在使用Lombok时maven install 找不到符号的问题
java·数据库·maven
间彧1 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
笨蛋少年派2 小时前
JAVA基础语法
java·开发语言
Haooog2 小时前
654.最大二叉树(二叉树算法)
java·数据结构·算法·leetcode·二叉树
我真的是大笨蛋2 小时前
依赖倒置原则(DIP)
java·设计模式·性能优化·依赖倒置原则·设计规范
brzhang2 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
东方芷兰3 小时前
JavaWeb 课堂笔记 —— 20 SpringBootWeb案例 配置文件
java·开发语言·笔记·算法·log4j·intellij-idea·lua