第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
用于组合future1
和future2
的结果。只有当这两个Future都完成时,才会调用thenCombine
里的函数。
第4章:异步操作和链式调用
异步操作的力量
异步操作是指在一个线程中启动一个任务,让它在另一个线程中运行,从而不阻塞当前线程的执行。这在处理耗时任务时特别有用。举个例子,假设咱们要查询数据库,然后处理查询结果。如果同步执行,整个程序都得等着数据库查询完成,这就浪费了宝贵的时间。但如果用CompletableFuture实现异步,就可以在查询数据库的同时做其他事情。
链式调用的魅力
链式调用则是指一系列操作依次执行,前一个操作的结果作为下一个操作的输入。CompletableFuture支持多种链式调用方法,比如thenApply
, thenAccept
和thenRun
。
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);
在这个例子中,future1
和future2
代表两个独立的异步操作。只有当两者都完成时,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提供了多种执行异步任务的方法,比如runAsync
和supplyAsync
。默认情况下,它们使用公共的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章:总结
-
异步编程的强大工具:CompletableFuture为Java异步编程提供了强大的支持,让处理并发任务变得更简单、更灵活。
-
简化复杂逻辑:通过链式调用和组合多个异步任务,CompletableFuture能够帮助咱们以清晰的方式处理复杂的业务逻辑。
-
异常处理的优雅方式:CompletableFuture提供了一套完整的异常处理框架,让咱们能够更好地控制和管理异步代码中的错误情况。