一、引言
在如今高并发的互联网时代,异步编程已成为Java开发者必须掌握的核心技能。传统的Future虽然提供了异步计算的雏形,但其功能单一、缺乏组合能力,难以应对复杂的异步场景。而CompletableFuture的出现,彻底改变了这一局面。无论我们是要优化现有系统性能,还是构建高响应的微服务架构,掌握CompletableFuture都将使我们的异步代码如虎添翼。
二、从 Future 到 CompletableFuture
在 Java 的并发编程领域中,异步操作是提升程序性能和响应性的关键手段。早期,Java 引入了Future接口,为异步编程提供了基础支持。Future允许我们提交一个异步任务,然后通过它来检查任务是否完成,获取任务的执行结果,或者取消任务。
比如,我们有一个简单的任务,计算两个数的乘积,但这个计算可能需要一些时间。使用Future可以这样实现:
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 模拟耗时操作
Thread.sleep(2000);
return multiply(3, 5);
}
});
System.out.println("主线程继续执行其他任务...");
// 获取异步任务的结果,如果任务未完成,这一步会阻塞
Integer result = future.get();
System.out.println("计算结果: " + result);
executor.shutdown();
}
private static int multiply(int a, int b) {
return a * b;
}
}
在这段代码中,我们通过ExecutorService提交了一个实现Callable接口的任务。Callable接口的call方法包含了具体的计算逻辑,这里是计算两个数的乘积,并模拟了 2 秒的耗时操作。主线程在提交任务后,可以继续执行其他任务,而不需要等待计算完成。最后,通过future.get()方法获取异步任务的结果,这一步会阻塞主线程,直到异步任务完成。
然而,Future存在一些局限性。例如,它缺乏对异步任务的精细控制和编排能力,获取结果时容易导致阻塞,并且难以处理多个异步任务之间的依赖关系和组合操作。例如,当我们需要执行多个异步任务,并在它们全部完成后进行下一步操作时,使用Future会变得比较繁琐。
为了解决这些问题,Java 8 引入了CompletableFuture。CompletableFuture不仅实现了Future接口,还实现了CompletionStage接口,提供了更强大、更灵活的异步编程能力。它允许我们以链式调用的方式处理异步任务,轻松地实现任务的组合、编排和异常处理,使得异步编程更加简洁和高效。
三、CompletableFuture 核心方法详解
3.1 创建异步任务
CompletableFuture提供了supplyAsync和runAsync两个静态方法来创建异步任务。supplyAsync方法用于执行有返回值的异步任务,它接收一个Supplier接口的实现作为参数,Supplier的get方法包含了具体的异步操作逻辑,执行完成后会返回一个CompletableFuture对象,该对象封装了异步任务的执行结果。例如:
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 3 * 5;
});
在这个例子中,supplyAsync方法内部的Supplier实现会在一个异步线程中执行,模拟了 2 秒的耗时操作后返回两个数的乘积。
而runAsync方法用于执行无返回值的异步任务,它接收一个Runnable接口的实现作为参数,Runnable的run方法包含了异步操作的逻辑。例如:
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这是一个无返回值的异步任务");
});
这里runAsync方法中的Runnable实现同样在异步线程中执行,模拟 1 秒耗时操作后打印一条信息。
两者的主要差异在于是否有返回值,supplyAsync适用于需要获取异步任务执行结果的场景,比如异步计算数据、查询数据库等操作;runAsync则适用于只需要执行异步操作,不需要返回结果的场景,例如异步记录日志、发送通知等。
3.2 结果处理与转换
当异步任务执行完成后,我们常常需要对结果进行处理或转换。CompletableFuture提供了thenApply、thenAccept和thenRun等方法来实现这些操作。
thenApply方法接收一个Function接口的实现作为参数,它会将前一个异步任务的结果作为参数传递给这个Function,经过处理后返回一个新的CompletableFuture对象,该对象封装了处理后的结果。例如:
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<String> resultFuture = future3.thenApply(result -> "结果是:" + result);
resultFuture.thenAccept(System.out::println);
在这段代码中,首先通过supplyAsync创建了一个返回值为 10 的异步任务,然后使用thenApply方法将这个结果转换为一个字符串,最后通过thenAccept方法消费这个结果并打印输出。
thenAccept方法接收一个Consumer接口的实现作为参数,它会将前一个异步任务的结果传递给这个Consumer,但不会返回新的结果,即返回的CompletableFuture的泛型为Void。例如:
CompletableFuture.supplyAsync(() -> 20)
.thenAccept(result -> System.out.println("接收到的结果是:" + result));
这里supplyAsync返回的结果 20 被传递给thenAccept中的Consumer,并在Consumer中进行打印操作。
thenRun方法接收一个Runnable接口的实现作为参数,它不关心前一个异步任务的结果,只是在前一个任务完成后执行这个Runnable。例如:
CompletableFuture.supplyAsync(() -> 30)
.thenRun(() -> System.out.println("异步任务已完成,不关心结果"));
这段代码中,supplyAsync返回结果 30,但thenRun并不处理这个结果,只是在前一个任务完成后打印一条信息。
3.3 任务组合与编排
在实际应用中,我们经常需要组合多个异步任务,按照一定的顺序或条件执行。CompletableFuture提供了thenCombine、thenAcceptBoth、runAfterBoth和allOf等方法来满足这些需求。
thenCombine方法用于将两个CompletableFuture的结果进行组合。它接收另一个CompletableFuture和一个BiFunction作为参数,当两个CompletableFuture都执行完成后,会将它们的结果作为参数传递给BiFunction,并返回一个新的CompletableFuture,该CompletableFuture封装了BiFunction的处理结果。例如:
CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<Integer> future5 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> combinedFuture = future4.thenCombine(future5, (a, b) -> a * b);
combinedFuture.thenAccept(System.out::println);
这里future4和future5分别异步计算 5 和 3,thenCombine方法将这两个结果相乘,并返回一个新的CompletableFuture,最终打印出结果 15。
thenAcceptBoth方法与thenCombine类似,也是等待两个CompletableFuture都完成后执行操作,但它接收的是一个BiConsumer,不会返回新的结果,即返回的CompletableFuture泛型为Void。例如:
CompletableFuture.supplyAsync(() -> "Hello")
.thenAcceptBoth(CompletableFuture.supplyAsync(() -> "World"), (a, b) -> System.out.println(a + " " + b));
这段代码中,两个异步任务分别返回 "Hello" 和 "World",thenAcceptBoth将这两个结果组合并打印输出 "Hello World"。
runAfterBoth方法则是在两个CompletableFuture都完成后,执行一个Runnable,它不关心两个任务的结果。例如:
CompletableFuture.supplyAsync(() -> 10)
.runAfterBoth(CompletableFuture.supplyAsync(() -> 20), () -> System.out.println("两个任务都完成了"));
这里两个异步任务分别返回 10 和 20,但runAfterBoth只关注任务是否完成,在两个任务都完成后打印一条信息。
allOf方法用于等待多个CompletableFuture都完成。它接收一个CompletableFuture数组作为参数,返回一个新的CompletableFuture,只有当所有传入的CompletableFuture都完成时,这个新的CompletableFuture才会完成。例如:
CompletableFuture<Integer> future6 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> future7 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(future6, future7);
allOfFuture.join();
System.out.println("所有任务都已完成");
在这个例子中,allOf方法等待future6和future7都完成后,allOfFuture才会完成,然后打印出 "所有任务都已完成"。
3.4 异常处理
异步任务在执行过程中可能会发生异常,CompletableFuture提供了exceptionally、handle和whenComplete等方法来处理异常。
exceptionally方法用于在异步任务发生异常时,执行一个回调方法。它接收一个Function作为参数,这个Function的参数是异常对象,返回值是在异常情况下的处理结果。例如:
CompletableFuture<Integer> future8 = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("任务执行失败");
}
return 10;
});
CompletableFuture<Integer> result = future8.exceptionally(ex -> {
System.out.println("捕获到异常:" + ex.getMessage());
return -1;
});
result.thenAccept(System.out::println);
在这段代码中,supplyAsync内部的任务有一定概率抛出异常,exceptionally方法捕获到异常后,打印异常信息并返回 - 1 作为处理结果。
handle方法则可以同时处理正常结果和异常情况。它接收一个BiFunction作为参数,第一个参数是正常情况下的结果,第二个参数是异常对象。无论任务是正常完成还是发生异常,都会执行这个BiFunction,并返回一个新的CompletableFuture。例如:
CompletableFuture<Integer> future9 = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("任务执行失败");
}
return 20;
});
CompletableFuture<String> result2 = future9.handle((res, ex) -> {
if (ex != null) {
System.out.println("捕获到异常:" + ex.getMessage());
return "处理异常结果";
} else {
return "正常结果:" + res;
}
});
result2.thenAccept(System.out::println);
这里handle方法根据任务执行情况返回不同的结果,正常时返回包含结果的字符串,异常时返回处理异常的字符串。
whenComplete方法与handle类似,也是在任务完成时执行回调,但它不会返回新的结果,只是将结果和异常传递给回调方法。例如:
CompletableFuture<Integer> future10 = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("任务执行失败");
}
return 30;
});
future10.whenComplete((res, ex) -> {
if (ex != null) {
System.out.println("捕获到异常:" + ex.getMessage());
} else {
System.out.println("正常结果:" + res);
}
});
这段代码中,whenComplete方法只是打印出任务执行的结果或异常信息,不进行结果的转换或返回新的CompletableFuture。通过这些异常处理方法,我们可以更加优雅地处理异步任务中的异常情况,提高程序的健壮性和稳定性。
四、CompletableFuture 应用场景实战
4.1 并行计算
在科学计算、数据分析等领域,经常需要进行大量的数值计算,这些计算往往耗时较长。使用CompletableFuture可以将计算任务并行化,充分利用多核 CPU 的优势,显著提高计算效率。
假设我们有一个需求,计算从 1 到 1000000 的整数之和。如果使用传统的单线程方式,代码如下:
public class TraditionalSum {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
long sum = 0;
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
long endTime = System.currentTimeMillis();
System.out.println("计算结果: " + sum);
System.out.println("单线程计算耗时: " + (endTime - startTime) + " 毫秒");
}
}
在上述代码中,通过一个简单的for循环对 1 到 1000000 的整数进行累加求和,这种方式在计算过程中,主线程会被阻塞,直到计算完成。
接下来,我们使用CompletableFuture来实现并行计算。思路是将整个计算任务拆分成多个子任务,每个子任务在独立的线程中并行执行,最后将各个子任务的结果汇总得到最终结果。代码示例如下:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureSum {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long startTime = System.currentTimeMillis();
// 拆分任务,将1到1000000分成10个子任务,每个子任务计算100000个数的和
int taskCount = 10;
CompletableFuture<Long>[] futures = new CompletableFuture[taskCount];
for (int i = 0; i < taskCount; i++) {
final int start = i * 100000 + 1;
final int end = (i + 1) * 100000;
futures[i] = CompletableFuture.supplyAsync(() -> calculateSum(start, end));
}
// 汇总结果
CompletableFuture<Long> allFutures = CompletableFuture.allOf(futures).thenApply(v -> {
long sum = 0;
for (CompletableFuture<Long> future : futures) {
try {
sum += future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return sum;
});
long sum = allFutures.get();
long endTime = System.currentTimeMillis();
System.out.println("计算结果: " + sum);
System.out.println("CompletableFuture并行计算耗时: " + (endTime - startTime) + " 毫秒");
}
private static long calculateSum(int start, int end) {
long sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
}
在这段代码中,首先创建了一个包含 10 个CompletableFuture的数组,每个CompletableFuture负责计算 100000 个数的和。然后通过CompletableFuture.allOf方法等待所有子任务完成,最后使用thenApply方法汇总各个子任务的结果。通过这种并行计算方式,能够充分利用多核 CPU 的计算资源,大大缩短计算时间。在实际运行中,你会发现CompletableFuture并行计算的耗时明显低于单线程计算的耗时 。
4.2 异步 I/O 操作
在进行文件读取、网络请求等 I/O 操作时,这些操作往往比较耗时,如果在主线程中同步执行,会导致主线程长时间阻塞,降低程序的响应性。使用CompletableFuture可以将 I/O 操作异步化,避免主线程阻塞,提高程序的整体性能。
以读取文件内容为例,假设我们有一个文本文件example.txt,现在需要读取其内容并进行处理。传统的同步读取方式如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TraditionalFileRead {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String filePath = "example.txt";
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("文件内容: " + content.toString());
System.out.println("同步读取文件耗时: " + (endTime - startTime) + " 毫秒");
}
}
上述代码通过BufferedReader逐行读取文件内容,在读取过程中,主线程会一直等待 I/O 操作完成,期间无法执行其他任务。
接下来,使用CompletableFuture实现异步读取文件内容。我们将文件读取操作放在一个异步任务中执行,主线程可以继续执行其他任务,当文件读取完成后,通过回调函数处理读取到的内容。代码示例如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
public class CompletableFutureFileRead {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String filePath = "example.txt";
CompletableFuture.supplyAsync(() -> readFileContent(filePath))
.thenAccept(content -> {
long endTime = System.currentTimeMillis();
System.out.println("文件内容: " + content);
System.out.println("异步读取文件耗时: " + (endTime - startTime) + " 毫秒");
})
.exceptionally(ex -> {
ex.printStackTrace();
return null;
});
// 主线程可以继续执行其他任务
System.out.println("主线程继续执行其他操作...");
}
private static String readFileContent(String filePath) {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
return content.toString();
}
}
在这段代码中,CompletableFuture.supplyAsync方法将文件读取操作封装成一个异步任务,在后台线程中执行。thenAccept方法用于处理异步任务完成后的结果,即文件内容。exceptionally方法用于捕获异步任务执行过程中可能出现的异常。在执行过程中,主线程在提交异步任务后,立即打印 "主线程继续执行其他操作...",然后继续执行其他任务,而无需等待文件读取完成,大大提高了程序的响应性。
4.3 多任务协同处理
在复杂的业务场景中,常常涉及多个异步任务之间的协同工作,例如在电商系统中,一个订单的处理可能涉及库存查询、价格计算、优惠券验证、物流信息获取等多个异步操作,并且这些操作之间存在一定的依赖关系和执行顺序。CompletableFuture提供了强大的任务编排能力,能够轻松实现这些复杂的业务逻辑。
假设我们正在开发一个电商下单系统,当用户下单时,需要完成以下几个异步任务:
- 查询商品库存:检查商品的库存数量是否足够。
- 计算订单价格:根据商品价格、数量以及可能的折扣计算订单总价。
- 验证优惠券:检查用户输入的优惠券是否有效,并计算优惠后的价格。
- 获取物流信息:根据用户地址获取物流配送信息。
只有当以上所有任务都成功完成后,才能确认订单并进行后续处理。使用CompletableFuture实现这个业务场景的代码示例如下:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class EcommerceOrder {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 模拟商品ID、用户ID、优惠券码和地址
int productId = 123;
int userId = 456;
String couponCode = "SAVE10";
String address = "北京市海淀区";
// 查询商品库存
CompletableFuture<Integer> stockFuture = CompletableFuture.supplyAsync(() -> checkStock(productId));
// 计算订单价格
CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> calculatePrice(productId));
// 验证优惠券
CompletableFuture<Double> couponFuture = priceFuture.thenCombine(stockFuture, (price, stock) -> validateCoupon(couponCode, price, stock));
// 获取物流信息
CompletableFuture<String> shippingFuture = CompletableFuture.supplyAsync(() -> getShippingInfo(address));
// 当所有任务完成后,确认订单
CompletableFuture.allOf(stockFuture, priceFuture, couponFuture, shippingFuture).thenRun(() -> {
try {
int stock = stockFuture.get();
double finalPrice = couponFuture.get();
String shippingInfo = shippingFuture.get();
confirmOrder(productId, userId, finalPrice, stock, shippingInfo);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}).join();
}
private static int checkStock(int productId) {
// 模拟查询库存逻辑,返回库存数量
System.out.println("查询商品库存...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}
private static double calculatePrice(int productId) {
// 模拟计算价格逻辑,返回商品价格
System.out.println("计算订单价格...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100.0;
}
private static double validateCoupon(String couponCode, double price, int stock) {
// 模拟验证优惠券逻辑,返回优惠后的价格
System.out.println("验证优惠券...");
if ("SAVE10".equals(couponCode) && stock > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return price - 10;
}
return price;
}
private static String getShippingInfo(String address) {
// 模拟获取物流信息逻辑,返回物流信息
System.out.println("获取物流信息...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "预计3天送达";
}
private static void confirmOrder(int productId, int userId, double finalPrice, int stock, String shippingInfo) {
if (stock > 0) {
System.out.println("订单确认成功!");
System.out.println("商品ID: " + productId);
System.out.println("用户ID: " + userId);
System.out.println("最终价格: " + finalPrice);
System.out.println("库存数量: " + stock);
System.out.println("物流信息: " + shippingInfo);
} else {
System.out.println("库存不足,订单失败!");
}
}
}
在这段代码中,首先通过CompletableFuture.supplyAsync分别创建了查询库存、计算价格和获取物流信息的异步任务。然后,利用thenCombine方法将计算价格和查询库存的结果作为参数传递给验证优惠券的任务,实现了任务之间的依赖关系。最后,通过CompletableFuture.allOf方法等待所有任务完成,再调用thenRun方法确认订单。这种方式使得复杂的多任务协同处理变得清晰、简洁,大大提高了代码的可读性和可维护性 。
五、CompletableFuture 使用中的常见问题与解决方案
5.1 线程池配置
CompletableFuture默认使用ForkJoinPool.commonPool()作为线程池,这个默认线程池的核心线程数是 CPU 核心数减 1 。在一些情况下,使用默认线程池可能会带来问题。比如,当异步任务阻塞或执行时间过长时,可能会导致线程池耗尽,影响其他任务的执行。例如,在一个高并发的 Web 应用中,如果多个CompletableFuture任务执行的是数据库查询操作,而数据库连接出现问题导致查询长时间阻塞,那么默认线程池中的线程将被这些阻塞任务占用,其他异步任务就无法得到及时执行,从而影响整个应用的性能和响应速度。
为了避免这些问题,我们可以自定义线程池。通过手动创建线程池,并合理配置其参数,如核心线程数、最大线程数、线程存活时间和阻塞队列等,可以更好地控制异步任务的执行,提高程序的性能和稳定性。
以下是一个自定义线程池的配置示例:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 手动创建线程池
int corePoolSize = 10; // 核心线程数
int maxPoolSize = 20; // 最大线程数
long keepAliveTime = 60L; // 非核心线程空闲存活时间,单位为秒
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 有界队列,容量为100
RejectedExecutionHandler rejectionHandler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略,直接抛出异常
ExecutorService customExecutor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
rejectionHandler
);
// 使用自定义线程池提交异步任务
CompletableFuture.runAsync(() -> {
// 模拟异步任务
try {
Thread.sleep(2000);
System.out.println("异步任务执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, customExecutor);
// 关闭线程池,通常在应用程序结束时执行
customExecutor.shutdown();
}
}
在上述示例中,我们创建了一个ThreadPoolExecutor类型的自定义线程池。corePoolSize设置为 10,表示线程池在正常情况下会保持 10 个核心线程。maxPoolSize设置为 20,即线程池最多可以容纳 20 个线程。keepAliveTime为 60 秒,意味着当线程池中的线程数量超过核心线程数时,多余的非核心线程如果空闲时间超过 60 秒,就会被销毁。workQueue使用ArrayBlockingQueue,容量为 100,用于存储等待执行的任务。rejectionHandler采用AbortPolicy拒绝策略,当线程池和任务队列都已满,无法处理新的任务时,会直接抛出RejectedExecutionException异常 。
5.2 异常处理陷阱
在使用CompletableFuture时,异常处理机制与传统的try...catch有所不同,若不熟悉其特性,可能会遇到以下问题:
(1)异常被吞噬:在CompletableFuture的链式调用中,如果某个阶段发生异常并且没有适当处理,异常可能会被吞噬而不会传播到后续阶段。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<String> result = future.thenApply(i -> "结果是:" + i);
result.join(); // 此处不会抛出异常
在这个例子中,supplyAsync阶段抛出的异常没有正确传播到thenApply阶段,导致异常被隐藏,难以发现和调试。
(2)异常处理丢失:当使用exceptionally方法处理异常时,如果处理不当,可能会导致异常处理丢失。比如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<String> result = future.exceptionally(ex -> {
System.out.println("捕获到异常:" + ex.getMessage());
return "默认值";
});
result.join(); // 此处不会抛出异常,且异常信息可能未得到有效处理
这里虽然在exceptionally方法中打印了异常信息,但只是返回了默认值,异常没有被正确传播到后续阶段,可能导致更高层的调用者无法感知到异常的发生。
(3)堆栈追踪丢失:在处理异常并重新抛出时,可能会导致堆栈追踪信息丢失,这给调试带来困难。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<String> result = future.thenApply(i -> {
try {
return process(i);
} catch (Exception ex) {
throw new RuntimeException("处理结果时出错:" + ex.getMessage());
}
});
result.join(); // 此处堆栈追踪信息丢失,难以定位原始异常位置
在这个例子中,重新抛出的异常没有包含原始异常的堆栈追踪信息,使得我们难以确定异常最初发生的位置和原因。
(4)异常处理冗长:当处理多个CompletableFuture链时,如果每个阶段都需要处理异常,可能会导致代码变得冗长和复杂,难以维护。例如:
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务1执行失败");
});
CompletableFuture<String> future2 = future1.thenApply(i -> {
try {
return process(i);
} catch (Exception ex) {
throw new RuntimeException("处理任务1结果时出错:" + ex.getMessage());
}
}).exceptionally(ex -> {
System.out.println("捕获到任务1异常:" + ex.getMessage());
return "任务1默认值";
});
CompletableFuture<String> future3 = future2.thenApply(s -> {
try {
return process2(s);
} catch (Exception ex) {
throw new RuntimeException("处理任务2结果时出错:" + ex.getMessage());
}
}).exceptionally(ex -> {
System.out.println("捕获到任务2异常:" + ex.getMessage());
return "任务2默认值";
});
future3.join();
随着CompletableFuture链的增长,异常处理代码会变得越来越多,增加了代码的复杂度和维护难度。
针对这些问题,我们可以采用以下解决方案:
- 使用whenComplete方法:whenComplete方法可以在任务完成时触发回调函数,无论是正常完成还是发生异常。通过在whenComplete方法中处理异常,我们可以确保异常得到正确的传播和处理。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<String> result = future.thenApply(i -> "结果是:" + i)
.whenComplete((res, ex) -> {
if (ex != null) {
System.out.println("捕获到异常:" + ex.getMessage());
// 可以根据需要进行异常处理,如记录日志、返回默认值等
}
});
result.join();
- 正确使用exceptionally方法:在使用exceptionally方法处理异常时,确保正确地处理异常并重新抛出(如果需要),以便异常能够传播到后续阶段。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<Integer> result = future.exceptionally(ex -> {
System.out.println("捕获到异常:" + ex.getMessage());
// 进行必要的异常处理,如记录日志
throw new RuntimeException("重新抛出异常", ex); // 重新抛出异常,保留堆栈追踪信息
});
result.join();
- 统一异常处理:可以将异常处理逻辑封装成一个独立的方法,在CompletableFuture链中统一调用,避免重复的异常处理代码,使代码更加简洁和可维护。例如:
public class ExceptionHandlerUtil {
public static <T> Function<Throwable, T> handleException(String taskName) {
return ex -> {
System.out.println("捕获到" + taskName + "的异常:" + ex.getMessage());
// 记录日志等操作
// 返回默认值或根据异常类型进行不同处理
return null;
};
}
}
// 使用示例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("任务执行失败");
});
CompletableFuture<Integer> result = future.exceptionally(ExceptionHandlerUtil.handleException("任务1"));
result.join();
5.3 阻塞操作误区
在使用CompletableFuture时,一个常见的误区是在迭代中使用阻塞方法,如join或get。例如:
List<CompletableFuture<Integer>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 模拟异步任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i * 2;
});
futures.add(future);
}
for (CompletableFuture<Integer> future : futures) {
Integer result = future.join(); // 在迭代中使用阻塞方法
System.out.println("结果:" + result);
}
在这个例子中,虽然使用了CompletableFuture来创建异步任务,但在迭代中使用join方法获取结果时,会导致主线程阻塞,依次等待每个异步任务完成,这就失去了异步编程的优势。这种做法不仅会阻塞主线程,使主线程无法执行其他任务,还无法充分利用并行性,降低了程序的执行效率。此外,在复杂的依赖链中,这种阻塞操作还可能引发死锁问题,例如两个线程相互等待对方的CompletableFuture完成,导致程序陷入僵局。
正确的做法是让所有异步任务同时运行,在最终需要结果时再统一处理。可以通过CompletableFuture.allOf方法等待所有异步任务完成,然后使用thenApply或thenAccept等方法来处理结果。例如:
List<CompletableFuture<Integer>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 模拟异步任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i * 2;
});
futures.add(future);
}
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.thenApply(v -> {
List<Integer> results = new ArrayList<>();
for (CompletableFuture<Integer> future : futures) {
try {
results.add(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return results;
}).thenAccept(results -> {
for (Integer result : results) {
System.out.println("结果:" + result);
}
}).exceptionally(ex -> {
System.out.println("处理结果时发生异常:" + ex.getMessage());
return null;
});
在这段代码中,首先创建了多个异步任务并添加到futures列表中。然后使用CompletableFuture.allOf方法等待所有任务完成,接着通过thenApply方法收集所有任务的结果,最后使用thenAccept方法处理这些结果。通过这种方式,所有异步任务可以并行执行,主线程不会被阻塞,提高了程序的执行效率和响应性 。
六、总结与展望
CompletableFuture作为 Java 8 引入的强大异步编程工具,极大地简化了异步任务的处理。它克服了传统Future的局限性,提供了丰富的方法用于创建、组合、编排异步任务以及处理异常,使得异步编程更加灵活、高效和易于维护。
从核心方法来看,CompletableFuture的创建方法(supplyAsync和runAsync)方便我们启动异步任务;结果处理与转换方法(thenApply、thenAccept、thenRun等)让我们能在任务完成后对结果进行各种操作;任务组合与编排方法(thenCombine、thenAcceptBoth、allOf等)则满足了复杂业务场景中多任务协同的需求;而异常处理方法(exceptionally、handle、whenComplete等)确保我们能优雅地处理异步任务执行过程中可能出现的异常情况 。
在实际应用中,CompletableFuture在并行计算、异步 I/O 操作和多任务协同处理等场景中都展现出了显著的优势,能够有效提升程序的性能和响应性,使代码结构更加清晰。然而,在使用CompletableFuture时,我们也需要注意线程池配置、异常处理和避免阻塞操作等问题,以充分发挥其优势并确保程序的稳定性和高效性。
最后希望大家在今后的项目开发中,能够熟练运用CompletableFuture,充分挖掘其潜力,提升项目的质量和性能。
最近整理了各板块和大厂的面试题以及简历模板(不同年限的都有),涵盖高并发,分布式等面试热点问题,足足有大几百页,需要的可以滴滴(v:bxlj_jcj),备注面试