一、引言
Future异步编排的过程中的主要局限性在于它不能很好地处理异步任务的结果和异常。具体来说,Future有以下几个局限性:
- 无法组合多个异步任务: Future只能等待单个异步任务完成,并且不能将多个异步任务组合在一起执行。
- 阻塞式获取结果: Future.get方法是一个阻塞式的调用,这意味着它会一直等待异步任务完成并返回结果,如果任务需要很长时间才能完成,就会导致程序一直阻塞。
- 无法取消任务: 如果异步任务已经开始执行,那么就无法取消它,即使任务已经超时或者不再需要。
- 无法处理异常: 如果异步任务出现了异常,Future只能通过get方法抛出ExecutionException异常来处理,而不能像try-catch语句一样优雅地处理异常。
针对Future异步编排过程中的以上种种缺点,CompletableFuture隆重登场。。。
二、概述
CompletableFuture
是Java 8引入的一种异步编程的工具,可以实现非阻塞式的处理。它提供了很多方法来组合和执行异步操作,并且支持异常处理、超时等功能。CompletableFuture
采用链式编程方式,允许将多个操作顺序或者并行地组合在一起,从而简化了异步编程的流程。通过使用CompletableFuture
,程序员能够更加灵活地控制异步任务的执行流程,提高应用程序的并发性和性能。
相比于Future
,CompletableFuture
具有以下几个优点:
- 支持组合多个异步任务:
CompletableFuture
可以通过thenCompose
、thenCombine
等方法将多个异步任务组合在一起执行,并且提供了更加灵活的流式调用方式,使得异步编程的代码更加简洁易读。 - 非阻塞式获取结果:
CompletableFuture
提供了回调函数的方式来处理异步任务的结果和异常,这意味着程序不需要一直阻塞等待任务完成,可以在任务完成后回调函数中处理结果。 - 可以取消任务:
CompletableFuture
支持cancel
方法来取消任务的执行,并且可以设置超时时间来避免程序一直等待。 - 异常处理更加灵活:
CompletableFuture
提供了exceptionally
、handle
等方法来处理异步任务的异常,使得代码异常处理更加优雅。
总之,CompletableFuture
提供了更加灵活、高效、可靠的异步编程方式,可以大大提高程序的并发性和性能。
三、功能介绍
CompletableFuture可以分为以下几类:
1. 创建/构造方法
- CompletableFuture.supplyAsync(): 异步执行一个有返回值的任务,并返回CompletableFuture对象(
supply
有返回值)。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello World");
上述代码异步执行了一个返回字符串"Hello World"的任务,在执行完毕后,返回一个CompletableFuture
对象。
- CompletableFuture.runAsync(): 异步执行一个无返回值的任务,并返回CompletableFuture对象。(无返回值)
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Void CompletableFuture!");
});
上述代码异步执行了一个没有返回值的任务,在执行完毕时,返回一个CompletableFuture对象。
- CompletableFuture.completedFuture(): 创建一个已经完成的CompletableFuture对象,可以用来替代null作为某些函数的返回值。
CompletableFuture<String> future = CompletableFuture.completedFuture("Already completed");
上述代码创建了一个已经完成的CompletableFuture对象future,并将其设置为返回字符串"Already completed"。
2. 转换方法
- thenApply(): 对上一个阶段的结果进行转换,并返回一个新的CompletableFuture对象(入参为功能型函数式接口)。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello World")
.thenApply(s -> s.toUpperCase());
上述代码首先异步执行了一个返回字符串"Hello World"的任务,并将其转换为大写字符串,最终返回一个CompletableFuture对象future。
- thenAccept(): 对上一个阶段的结果进行消费,但不返回任何结果(入参为消费型函数式接口)
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello World")
.thenAccept(s -> System.out.println("Received message: " + s));
上述代码异步执行了一个返回字符串"Hello World"的任务,在执行完毕时,将其打印出来,最终返回一个CompletableFuture对象。
- thenRun(): 在上一个阶段执行完毕后执行一个Runnable操作,不关心上一个阶段的结果(入参为Runnable函数式接口,无参数无返回值)
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello World")
.thenRun(() -> System.out.println("Done!"));
上述代码异步执行了一个返回字符串"Hello World"的任务,在执行完毕时,输出"Done!",最终返回一个CompletableFuture对象。
3. 组合方法
- thenCompose(): 将两个CompletableFuture对象通过一个函数进行组合,返回一个新的CompletableFuture对象。
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5)
.thenCompose(i -> CompletableFuture.supplyAsync(() -> i * 10));
上述代码首先异步执行了一个返回整数5的任务。在执行完毕时,将其作为参数传递给第二个CompletableFuture对象,异步执行返回整数i×10的任务,并将其作为最终结果,返回一个新的CompletableFuture对象future。
- thenCombine(): 将两个CompletableFuture对象的结果通过一个BiFunction进行组合,并返回一个新的CompletableFuture对象。
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5)
.thenCombine(CompletableFuture.supplyAsync(() -> 10), (a, b) -> a + b);
上述代码异步执行了两个任务返回整数5和10。在两个任务都执行完毕后,将它们的结果通过一个BiFunction(lambda表达式)相加,并将结果作为最终结果,返回一个新的CompletableFuture对象future。
- anyOf(): 当任意一个CompletableFuture对象完成时,返回一个新的CompletableFuture对象,其结果是第一个完成的对象的结果。
CompletableFuture<Object> future = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> "Hello"),
CompletableFuture.supplyAsync(() -> "World")
);
上述代码同时异步执行了两个任务,返回字符串"Hello"和"World"。当其中任何一个任务执行完毕时,都会返回一个新的CompletableFuture对象future,其结果是第一个完成的任务的结果。
- allOf(): 当所有的CompletableFuture对象都完成时,返回一个新的CompletableFuture对象,其结果为空。
CompletableFuture<Void> future = CompletableFuture.allOf(
CompletableFuture.runAsync(() -> System.out.println("Task 1")),
CompletableFuture.runAsync(() -> System.out.println("Task 2"))
);
上述代码同时异步执行了两个无返回值的任务,打印出"Task 1"和"Task 2"。当这两个任务都执行完毕时,将返回一个新的CompletableFuture对象future,其结果为空。
4. 异常处理方法
- exceptionally(): 当CompletableFuture对象发生异常时,可以使用该方法处理异常并返回一个新的CompletableFuture对象。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (1 == 1) {
throw new RuntimeException("exception");
}
return "result";
}).exceptionally(e -> "default");
上述代码异步执行了一个任务,在任务执行过程中抛出一个RuntimeException异常。在异常发生时,将会返回一个新的CompletableFuture对象future,其结果为"default"字符串。
- handle(): 可以处理上一个阶段的结果或者异常,并返回一个新的CompletableFuture对象。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (1 == 1) {
throw new RuntimeException("exception");
}
return "result";
}).handle((s, t) -> s != null ? s : t.getMessage());
上述代码异步执行了一个任务,在任务执行过程中抛出一个RuntimeException异常。在异常发生时,将会返回一个新的CompletableFuture对象future,其结果为异常的错误信息。如果没有发生异常,则返回原先异步执行的任务的结果。
其他方法
- cancel(): 取消CompletableFuture对象的执行。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 长时间的计算
return "result";
});
boolean cancelled = future.cancel(true);
上述代码异步执行了一个任务,但在任务还没有完成前就取消了它的执行,并返回一个boolean值表示是否成功取消。
- get(): 获取CompletableFuture对象的结果,会阻塞当前线程直到结果可用。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 长时间的计算
return "result";
});
String result = future.get();
上述代码异步执行了一个任务,使用get()方法获取该任务的结果。如果该任务还没有完成,则阻塞当前线程直到结果可用。
- join(): 获取CompletableFuture对象的结果,不会抛出异常。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 长时间的计算
return "result";
});
String result = future.join();
上述代码异步执行了一个任务,使用join()方法获取该任务的结果。与get()方法不同的是,如果该任务还没有完成,则join()方法会一直等待,直到结果可用,而不会抛出异常。
- completeExceptionally(): 主动抛出一个异常,使CompletableFuture对象的执行结束。
CompletableFuture<String> future = new CompletableFuture<>();
future.completeExceptionally(new RuntimeException("exception"));
上述代码创建了一个未完成的CompletableFuture对象future,并在其上主动抛出一个RuntimeException异常,使其执行结束。
四、常见的任务异步编排
下面是几个常见的经典的任务异步编排案例。
- 串行执行两个异步任务,将它们的结果传递给第三个异步任务:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 1: " + Thread.currentThread().getName());
return 2;
}).thenApplyAsync(result -> {
System.out.println("Task 2: " + Thread.currentThread().getName());
return result * 3;
}).thenApplyAsync(result -> {
System.out.println("Task 3: " + Thread.currentThread().getName());
return result + 1;
});
上述代码中,我们使用了thenApplyAsync()方法将两个异步任务串联起来,将第一个任务的结果传递给第二个任务,并将第二个任务的结果传递给第三个任务。最终得到的CompletableFuture对象future将包含第三个任务的结果。
- 并行执行多个异步任务,等待所有任务完成后进行聚合:
List<CompletableFuture<Integer>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int finalI = i;
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep((5 - finalI) * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return finalI;
});
futures.add(future);
}
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
CompletableFuture<List<Integer>> allResults = allFutures.thenApply(v -> {
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
});
System.out.println(allResults.get());
上述代码中,我们使用了CompletableFuture.allOf()方法并行执行了5个异步任务,并等待它们全部完成后进行聚合。最终得到的CompletableFuture对象allResults将包含所有任务的结果列表。
- 并行执行多个异步任务,只要有一个任务完成就返回结果:
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int finalI = i;
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep((5 - finalI) * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Result " + finalI;
});
futures.add(future);
}
CompletableFuture<Object> firstResult = CompletableFuture.anyOf(
futures.toArray(new CompletableFuture[0])
);
System.out.println(firstResult.get());
上述代码中,我们使用了CompletableFuture.anyOf()方法并行执行了5个异步任务,只要有一个任务完成就返回它的结果。最终得到的CompletableFuture对象firstResult将包含第一个完成任务的结果。
五、关于自定义线程池特别说明
这要根据具体的应用场景来决定。如果只是简单地执行一些较短的任务,可以直接使用默认的线程池即可;而对于长时间运行的任务,或者需要更细粒度控制的任务,则建议自定义线程池。
使用默认线程池可能会导致以下问题:
-
线程数过多:ForkJoinPool.commonPool()的默认大小是CPU核心数-1,当一些较长时间运行的任务占用这些线程时,其他的短时间任务就可能无法在合理时间内得到执行,影响整个应用的性能。
-
线程数过少:如果应用中存在大量的短时间任务,但使用的是默认的线程池,由于线程池中的线程数较少,短时间任务很容易就被阻塞了,从而影响整个应用的性能。
-
无法达到最优的资源利用率:默认的线程池对于不同类型的任务可能没有针对性的优化,因此可能无法充分利用系统资源。
因此,在实际应用中,我们应该根据任务的特点和需求来选择合适的线程池,以达到更好的性能和效果。同时,需要注意线程池的创建和销毁也会带来一定的开销,因此应避免频繁地创建和销毁线程池。