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

发布时间:2024年01月10日

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

文章来源:https://blog.csdn.net/weixin_42116348/article/details/135513913
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。