一、引言
(一)简述 CompletableFuture 出现的背景
在 Java 并发编程中,传统的线程和同步机制如 Thread 类和 Runnable 接口提供了基本的并行执行能力,但它们存在一定局限,即无法获取线程执行的结果,没有返回值。为解决此问题,JDK1.5 引入了 Callable 和 Future 接口以及 Future 对应的实现类 FutureTask,通过 FutureTask 可以获取异步执行的结果。
然而,Future 接口在处理异步任务时仍然存在一些局限。例如,通过 Future 接口的 get 方法获取任务异步执行结果时,该方法会阻塞主线程,也就是异步任务没有完成,主线程会一直阻塞,直到任务结束。虽然它也提供了 isDone 方法来查看异步线程任务执行是否完成,可若采用轮询查看异步线程任务执行状态的方式,又会非常消耗 CPU 资源。并且对于一些复杂的异步操作任务的处理,可能需要各种同步组件来一起完成。
另外,Future 很难直接表述多个 Future 结果之间的依赖性,像将多个异步计算的结果合并成一个、等待 Future 集合中的所有任务都完成、在 Future 任务完成以后触发执行动作等需求,仅靠 Future 接口也较难实现,而且它还缺乏关于异常处理的方法。
为了解决这些问题,Java 8 引入了 CompletableFuture,它不仅实现了 Future 接口,还提供了丰富的 API 来支持异步编程,使得开发者可以更优雅地处理异步任务的执行、结果处理和异常处理,为异步编程带来了更多便利和强大的功能。
(二)点明文章重点
在日常的编程实践中,CompletableFuture 有着诸多巧妙且实用的用法,只是很多开发者可能还未充分掌握。本文就将聚焦于 CompletableFuture 的这些 “奇技淫巧”,比如它能够轻松地将多个异步任务串联或并行执行,并在任务完成后进行回调处理;支持自定义线程池,方便开发者灵活地管理线程资源,提高程序的并发性能和可维护性;还提供了像 thenApply、thenAccept、thenCombine、exceptionally、handle、whenComplete 等各种各样实用的方法,每个方法在不同场景下都能发挥独特作用,帮助我们更高效地处理异步任务及相关逻辑。接下来,文章会围绕这些巧妙用法、实用技巧展开详细讲解,通过具体的代码示例和场景分析,帮助读者更好地掌握和运用 CompletableFuture 进行异步编程,让大家能够在实际开发中充分发挥它的强大功能,编写出更简洁、高效、易维护的代码。
二、CompletableFuture 基础回顾
(一)与 Future 的关系及优势
CompletableFuture 是对 Future 接口的扩展和增强,它很好地弥补了 Future 在处理异步任务时存在的诸多不足。
首先,在并发执行多任务方面,Future 接口主要通过 get 方法获取任务异步执行结果,但该方法会阻塞主线程,若任务未完成,主线程只能一直等待,缺乏更灵活的并发控制机制。而 CompletableFuture 允许任务在后台线程中异步执行,不会阻塞主线程,能极大提高应用程序的响应性和性能,例如可以同时发起多个异步任务,让它们各自独立运行,互不干扰。
其次,针对任务链式调用的问题,Future 并没有提供便捷的方式来实现在一个计算任务完成后紧接着执行特定动作。但 CompletableFuture 借助实现的 CompletionStage 接口具备了强大的任务编排能力,它可以轻松地组织不同任务的运行顺序、规则以及方式,一个任务完成后能自然地触发后续阶段任务的执行,就像将多个任务串联在一条流水线上依次执行,实现链式调用,使代码逻辑更加清晰简洁。
再者,在组合多个任务方面,Future 很难直接表述多个 Future 结果之间的依赖性,像等待多个 Future 任务全部完成后再执行某个操作,或者根据多个任务执行的结果进行相应处理等需求,仅靠 Future 接口较难实现。而 CompletableFuture 提供了诸如 allOf、anyOf、thenCombine 等丰富的方法来支持多个任务的不同组合关系。比如 allOf 方法可以等待一组 CompletableFuture 全部完成,方便进行后续的统一处理;thenCombine 能够合并两个任务的结果,并返回一个新的 CompletableFuture,便于对多个任务的结果进行整合利用。
最后,在异常处理上,Future 接口中没有专门的关于异常处理的方法,而 CompletableFuture 提供了像 exceptionally、handle 等方法来处理任务执行过程中可能出现的异常情况。例如 exceptionally 方法可以在任务抛出异常时执行相应的逻辑,返回一个替代的结果或者进行一些补救措施,让程序的健壮性更强。
总之,CompletableFuture 在异步编程中相比 Future 接口有着显著的优势,为开发者处理复杂的异步任务场景提供了更强大、更便捷的工具。
(二)创建方式
1. 常用的 4 种创建方式介绍
CompletableFuture 源码中提供了四个常用的静态方法来创建 CompletableFuture 实例,它们有着不同的用法及区别,下面详细介绍一下:
- supplyAsync 方法:
-
- 此方法以 Supplier 函数式接口类型为参数,返回结果类型为 U。Supplier 接口的 get 方法是有返回值的(会阻塞),也就意味着通过 supplyAsync 创建的异步任务执行结束后能够返回相应的结果。例如:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟一些耗时操作,比如查询数据库获取数据等
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "查询到的数据";
});
- 当使用没有指定 Executor 的 supplyAsync 方法时,内部会使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。ForkJoinPool 是 Java 7 引入的一个用于并行执行任务的框架,它的默认线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。但如果所有 CompletableFuture 共享一个线程池,一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以在实际应用中,对于一些对资源隔离、性能要求较高的业务场景,建议使用指定 Executor 的重载方法来传入自定义的线程池。示例如下:
ExecutorService executorService = Executors.newFixedThreadPool(5);
CompletableFuture<String> futureWithExecutor = CompletableFuture.supplyAsync(() -> {
// 执行具体任务逻辑
return "自定义线程池执行的任务结果";
}, executorService);
- runAsync 方法:
-
- runAsync 方法以 Runnable 函数式接口类型为参数,没有返回结果,主要用于执行那些不需要返回值的异步操作,比如只是简单地进行一些日志记录、发送通知等操作。示例代码如下:
CompletableFuture<Void> voidFuture = CompletableFuture.runAsync(() -> {
System.out.println("执行无返回值的异步任务");
});
- 与 supplyAsync 类似,在不指定 Executor 时,同样默认使用 ForkJoinPool.commonPool() 作为线程池来执行异步代码,也可以通过传入自定义的 Executor 来指定线程池,像这样:
ExecutorService customExecutor = Executors.newCachedThreadPool();
CompletableFuture<Void> voidFutureWithExecutor = CompletableFuture.runAsync(() -> {
System.out.println("在自定义线程池中执行无返回值的任务");
}, customExecutor);
2. 不同创建方式示例
下面通过具体代码示例展示使用不同创建方式创建 CompletableFuture 实例的过程:
- 使用默认内置线程池 ForkJoinPool.commonPool() 创建:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCreationExample {
public static void main(String[] args) throws Exception {
// 使用supplyAsync创建有返回值的CompletableFuture,使用默认线程池
CompletableFuture<String> supplyAsyncFuture = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "通过supplyAsync在默认线程池执行任务得到的结果";
});
// 使用runAsync创建无返回值的CompletableFuture,使用默认线程池
CompletableFuture<Void> runAsyncFuture = CompletableFuture.runAsync(() -> {
System.out.println("通过runAsync在默认线程池执行无返回值的任务");
});
// 获取supplyAsync的结果(会阻塞直到任务完成)
String result = supplyAsyncFuture.get();
System.out.println("supplyAsync结果: " + result);
// 等待runAsync任务完成(虽然无返回值,但可以确保任务执行完毕)
runAsyncFuture.join();
}
}
在上述代码中,分别使用 supplyAsync 和 runAsync 方法基于默认的 ForkJoinPool.commonPool() 创建了异步任务,一个有返回值,一个无返回值,并展示了如何获取结果以及等待任务执行完毕的操作。
- 自定义线程池创建的情况:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureWithCustomThreadPoolExample {
public static void main(String[] args) throws Exception {
// 创建一个自定义的固定大小线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 使用supplyAsync结合自定义线程池创建有返回值的CompletableFuture
CompletableFuture<Integer> customSupplyAsyncFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("在自定义线程池中执行有返回值的任务,当前线程: " + Thread.currentThread().getName());
return 42;
}, executorService);
// 使用runAsync结合自定义线程池创建无返回值的CompletableFuture
CompletableFuture<Void> customRunAsyncFuture = CompletableFuture.runAsync(() -> {
System.out.println("在自定义线程池中执行无返回值的任务,当前线程: " + Thread.currentThread().getName());
}, executorService);
// 获取自定义线程池下supplyAsync的结果
Integer customResult = customSupplyAsyncFuture.get();
System.out.println("自定义线程池下supplyAsync结果: " + customResult);
// 等待自定义线程池下runAsync任务完成
customRunAsyncFuture.join();
// 关闭自定义线程池
executorService.shutdown();
}
}
这段代码首先创建了一个自定义的固定大小线程池,然后通过 supplyAsync 和 runAsync 方法传入该线程池来创建相应的 CompletableFuture 实例,展示了在自定义线程池环境下执行异步任务以及获取结果、关闭线程池等完整流程,方便开发者根据实际业务需求灵活地管理线程资源,提高程序的并发性能和可维护性。
三、CompletableFuture 的奇技淫巧
(一)结果获取的 4 种方式
在使用 CompletableFuture 时,获取结果是很重要的操作,它提供了四种方式来满足不同场景需求。
- get 方法:这是从 Future 接口中继承而来的获取结果方式。它会阻塞当前线程,直到异步任务执行完成并返回结果,如果任务执行过程中出现异常,会抛出相应的异常。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 42;
});
try {
Integer result = future.get();
System.out.println("获取到的结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
不过要注意,由于其阻塞特性,在实际使用中如果不合理安排调用位置,可能会影响程序的并发性能,所以通常建议放在合适的位置,避免主线程长时间阻塞等待。
- getNow 方法:此方法会立即尝试获取结果,如果任务已经执行完成,就返回执行结果或者执行过程中的异常;要是任务还未计算完成,那么就返回设定的valueIfAbsent值。示例如下:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100;
});
Integer result = future.getNow(0); // 假设任务还没完成,这里会返回0
System.out.println("获取到的结果: " + result);
这个方法适合那些不想长时间阻塞等待结果,希望能先拿到一个默认值进行后续处理的场景。
- join 方法:它用于等待异步任务完成并获取结果。如果异步任务已经完成,该方法会立即返回任务的执行结果;要是异步任务尚未完成,它就会阻塞当前线程,直至任务执行完成并返回结果为止。它和get方法类似,但join方法抛出的是 unchecked 异常(CompletionException),使用起来相对简洁一些,像这样:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "测试结果";
});
String result = future.join();
System.out.println("获取到的结果: " + result);
4. get(long timeout, TimeUnit unit) 方法:这个方法是在get方法基础上添加了超时处理机制。如果在指定的时间内未获取到结果,就会抛出超时异常。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 200;
});
try {
Integer result = future.get(2, TimeUnit.SECONDS); // 设置超时时间为2秒
System.out.println("获取到的结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
在实际应用中,当我们对异步任务的执行时间有预期,并且不希望无限制等待时,就可以采用这种方式来获取结果,避免因任务长时间未完成而导致程序出现性能问题。
不同的结果获取方式适用于不同的业务场景,开发者可以根据实际需求灵活选择使用,以便更好地处理异步任务的结果获取操作。
(二)异步回调方法
- thenRun/thenRunAsync 的使用与区别:
thenRun和thenRunAsync方法都是在某个任务执行完成后,接着去执行另一个无返回值的任务(通常是Runnable类型的任务),不过它们在使用线程池方面存在区别。
thenRun方法在执行后续任务时,会沿用上一个任务的线程池。比如下面的代码示例:
ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println("第一个任务执行,线程: " + Thread.currentThread().getName());
}, executorService);
CompletableFuture<Void> future2 = future1.thenRun(() -> {
System.out.println("thenRun执行的任务,线程: " + Thread.currentThread().getName());
});
在上述代码中,future2执行时会使用future1所传入的那个自定义线程池executorService,也就是它们共用同一个线程池。
而thenRunAsync方法,如果没有传入自定义线程池,会使用公用的ForkJoinPool线程池(不过在实际开发中,一般不推荐使用公用线程池,更建议传入自定义线程池来更好地管理资源和控制并发);当传入自定义线程池后,则会使用该自定义线程池来执行后续任务。示例如下:
ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> {
System.out.println("第三个任务执行,线程: " + Thread.currentThread().getName());
}, executorService);
CompletableFuture<Void> future4 = future3.thenRunAsync(() -> {
System.out.println("thenRunAsync执行的任务,线程: " + Thread.currentThread().getName());
}, executorService);
这里future4在执行时,虽然future3传入了自定义线程池executorService,但future4执行的任务使用的是ForkJoinPool线程池(如果没有额外传入自定义线程池的情况);而像上面代码传入了自定义线程池executorService,那么future4就会使用这个传入的自定义线程池来执行任务了。
通过合理运用这两个方法以及选择合适的线程池使用方式,可以根据业务场景对异步任务执行后的操作进行灵活编排,比如在任务完成后进行一些日志记录、资源清理等不需要返回值的后续操作。
- 其他回调方法介绍:
-
- thenAccept 方法:该方法表示在第一个任务执行完成后,执行第二个回调方法任务,并且会将第一个任务的执行结果作为入参传递到回调方法中,不过回调方法本身是没有返回值的。它常用于对异步任务的结果进行消费处理,例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 88;
});
future.thenAccept(result -> {
System.out.println("获取到的结果进行处理: " + result * 2);
});
在这个例子中,supplyAsync产生的异步任务结果会传递到thenAccept的回调函数中,然后进行相应的处理(这里是简单乘以 2 并打印),但整个thenAccept操作并没有返回新的结果供后续链式调用使用。
- thenApply 方法:和thenAccept类似,也是在第一个任务执行完成后执行第二个回调方法任务,同样会将第一个任务的执行结果作为入参传递到回调方法中,不过thenApply的回调方法是有返回值的,这就使得它可以方便地用于对异步任务结果进行转换并返回新的结果,以支持后续的链式调用。例如:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "123";
});
CompletableFuture<Integer> newFuture = future.thenApply(str -> {
return Integer.parseInt(str);
});
上述代码先是通过supplyAsync产生一个返回字符串类型结果的异步任务,然后利用thenApply将字符串结果转换为整数类型的结果,并返回一个新的CompletableFuture对象,后续可以继续基于这个新对象进行更多的操作,比如再接着调用其他回调方法等,实现异步任务结果的逐步处理和转换,构建起复杂的异步任务处理逻辑链。
- thenCombine 方法:它用于将两个任务的执行结果进行合并处理,并返回一个新的CompletableFuture。比如有两个独立的异步任务分别获取不同的数据,然后通过thenCombine可以将这两个任务的结果整合到一起进行后续操作,示例如下:
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
return 5;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
return 10;
});
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
return result1 + result2;
});
这里future1和future2分别返回不同的整数结果,通过thenCombine将两个结果相加后返回一个新的CompletableFuture,方便对多个异步任务结果进行整合利用,在很多涉及多数据源结果合并等业务场景中非常实用。
- thenCompose 方法:该方法会在某个任务执行完成后,将该任务的执行结果作为方法入参,去执行指定的方法,并且会返回一个新的CompletableFuture实例。如果这个新的CompletableFuture实例的result不为null,则返回一个基于该result新的CompletableFuture实例;如果该CompletableFuture实例为null,那就接着执行后续新任务。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10;
});
CompletableFuture<String> composedFuture = future.thenCompose(result -> {
return CompletableFuture.supplyAsync(() -> {
return "处理后的结果: " + result;
});
});
在这个示例中,先通过supplyAsync得到一个返回整数的异步任务结果,然后利用thenCompose基于这个结果再发起一个新的异步任务,对结果进行进一步处理并返回最终的字符串类型结果的CompletableFuture,可用于构建复杂的异步任务依赖和转换关系,让异步代码逻辑更加清晰和易于维护。
这些回调方法各有特点,在实际业务场景中,开发者可以根据具体需求,灵活选择和组合使用它们,实现对异步任务结果的多样化处理、任务之间的组合以及链式调用等操作,从而编写出高效、简洁且易维护的异步代码。
(三)异常处理技巧
在异步编程中,异常处理是至关重要的环节,CompletableFuture 提供了一些实用的方法来优雅地处理异步计算过程中可能出现的异常情况。
- exceptionally 方法:该方法允许我们在异步任务发生异常时执行一个备用的操作。它接收一个Function类型的参数,这个参数定义了在发生异常时应该执行的操作,并返回一个新的CompletableFuture对象。例如:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("出现异常啦");
});
CompletableFuture<Integer> result = future.exceptionally(ex -> {
System.out.println("捕获到异常: " + ex.getMessage());
return 0; // 返回备用值
});
System.out.println(result.join());
在上述代码中,原本的异步任务supplyAsync内部抛出了异常,但是通过exceptionally方法捕获到这个异常后,执行了相应的处理逻辑(这里是简单打印异常信息并返回一个默认值 0),然后返回了一个新的CompletableFuture对象,后续可以继续基于这个新对象进行操作,避免了异常直接导致程序中断或者后续逻辑无法执行的情况,提高了程序的健壮性。
- handle 方法:它与exceptionally方法类似,但功能更加强大,可以处理正常结果和异常结果。handle方法接收一个BiFunction参数,该参数定义了在任务完成时(无论正常完成还是出现异常)应该执行的操作,并返回一个新的CompletableFuture对象。示例如下:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10 / 0; // 这里会出现算术异常
});
CompletableFuture<Integer> handledFuture = future.handle((res, ex) -> {
if (ex!= null) {
System.out.println("出现异常: " + ex.getMessage());
return -1; // 异常时返回特定值
} else {
return res * 2; // 正常结果时进行相应处理
}
});
System.out.println(handledFuture.join());
在这个例子中,当异步任务出现异常时,handle方法中的逻辑会检测到异常并执行相应的异常处理操作(返回 -1);要是异步任务正常完成,就会按照正常结果的处理逻辑(这里是将结果乘以 2)进行操作,最后返回一个新的CompletableFuture对象,这样无论任务执行情况如何,都能进行合适的后续处理,方便统一对异步任务的结果进行管理和响应。
通过合理运用exceptionally、handle等异常处理方法,我们能够更好地应对异步任务执行过程中的各种不确定性,确保程序在出现异常时也能按照预期的逻辑进行处理,保证系统的稳定性和可靠性。
(四)多任务编排与执行
- 任务的串行执行:
在实际业务中,常常会遇到多个任务需要按顺序依次执行的情况,即一个任务依赖上一个任务的结果才能进行下一步操作,CompletableFuture 可以很方便地实现这种串行执行的场景。
例如,有一个业务流程,首先需要从数据库中查询用户信息,然后根据用户信息去获取该用户的订单列表,最后再统计订单的总金额。代码示例如下:
CompletableFuture<User> getUserFuture = CompletableFuture.supplyAsync(() -> {
// 模拟从数据库查询用户信息的操作
return getUserFromDB();
});
CompletableFuture<List<Order>> getOrdersFuture = getUserFuture.thenApply(user -> {
// 根据用户信息获取订单列表
return getOrdersByUser(user);
});
CompletableFuture<Integer> totalAmountFuture = getOrdersFuture.thenApply(orders -> {
// 统计订单总金额
return calculateTotalAmount(orders);
});
totalAmountFuture.thenAccept(total -> {
System.out.println("订单总金额为: " + total);
});
在上述代码中,getUserFuture首先发起异步任务查询用户信息,然后getOrdersFuture依赖getUserFuture的结果去获取订单列表,totalAmountFuture又依赖getOrdersFuture的结果来统计总金额,最后通过thenAccept对最终的总金额结果进行消费(这里只是简单打印)。通过这样的链式调用,实现了多个任务的串行执行,清晰地表达了任务之间的依赖关系,让整个业务流程的异步处理逻辑更加直观和易于维护。
- 任务的并行执行:
当我们有多个相互独立的任务,希望它们能同时执行,以提高执行效率,节省时间时,就可以利用 CompletableFuture 的allOf和anyOf方法来实现。
-
- allOf 方法:它可以等待一组CompletableFuture全部执行完成后,再进行后续操作。比如现在有三个独立的异步任务,分别是发送邮件、记录日志、更新缓存,我们希望这三个任务都完成后再进行一些统一的处理(比如统计本次操作的耗时等),示例代码如下:
CompletableFuture<Void> sendEmailFuture = CompletableFuture.runAsync(() -> {
// 模拟发送邮件操作
sendEmail();
});
CompletableFuture<Void> writeLogFuture = CompletableFuture.runAsync(() -> {
// 模拟记录日志操作
writeLog();
});
CompletableFuture<Void> updateCacheFuture = CompletableFuture.runAsync(() -> {
// 模拟更新缓存操作
updateCache();
});
CompletableFuture<Void> allFutures = CompletableFuture.allOf(sendEmailFuture, writeLogFuture, updateCacheFuture);
allFutures.thenRun(() -> {
System.out.println("所有任务都已完成,可以进行后续统一处理了");
});
在这个例子中,allFutures会等待sendEmailFuture、writeLogFuture和updateCacheFuture这三个任务都执行完毕后,才会执行后续的thenRun中的逻辑,实现了并行执行多个任务并在全部完成后进行统一处理的功能。
- anyOf 方法:该方法则是只要一组CompletableFuture中的任意一个任务完成,就可以进行后续操作了。例如有多个网络接口调用任务,只要有一个接口返回了数据,就可以先进行初步的展示或者下一步处理,代码示例如下:
CompletableFuture<String> apiCall1Future = CompletableFuture.supplyAsync(() -> {
// 模拟调用接口1获取数据
return callApi1();
});
CompletableFuture<String> apiCall2Future = CompletableFuture.supplyAsync(() -> {
// 模拟调用接口2获取数据
return callApi2();
});
CompletableFuture<String> apiCall3Future = CompletableFuture.supplyAsync(() -> {
// 模拟调用接口3获取数据
return callApi3();
});
CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(apiCall1Future, apiCall2Future, apiCall3Future);
anyFuture.thenAccept(result -> {
System.out.println("有一个任务完成了,获取到的结果: " + result);
});
在上述代码中,anyFuture只要apiCall1Future、apiCall2Future、apiCall3Future这三个异步任务中的任意一个完成并返回结果,就会执行thenAccept中的逻辑,对最先
四、CompletableFuture 在实际开发中的应用案例
(一)模拟查询场景
在实际开发中,常常会遇到需要从多个数据源获取信息的情况,比如查询用户信息和商品信息,然后进行整合处理。下面我们就以这个场景为例,展示如何运用 CompletableFuture 实现异步查询,提高查询效率,减少整体耗时。
假设我们有两个方法,一个用于查询用户信息,一个用于查询商品信息,代码示例如下:
import java.util.concurrent.CompletableFuture;
// 模拟查询用户信息的方法
public CompletableFuture<User> getUserInfo() {
return CompletableFuture.supplyAsync(() -> {
// 这里可以是实际从数据库或者其他数据源获取用户信息的逻辑,比如调用用户服务接口等
User user = new User();
user.setName("张三");
user.setAge(25);
// 模拟耗时操作,比如网络请求或者数据库查询等
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return user;
});
}
// 模拟查询商品信息的方法
public CompletableFuture<Product> getProductInfo() {
return CompletableFuture.supplyAsync(() -> {
// 实际获取商品信息的逻辑
Product product = new Product();
product.setName("手机");
product.setPrice(5000.0);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return product;
});
}
在上述代码中,我们通过supplyAsync方法分别创建了两个异步任务来查询用户信息和商品信息,它们会在后台线程中执行,不会阻塞主线程。
接下来,我们可以使用thenCombine方法将这两个异步任务的结果进行合并处理,示例如下:
CompletableFuture<User> userInfoFuture = getUserInfo();
CompletableFuture<Product> productInfoFuture = getProductInfo();
CompletableFuture<String> combinedFuture = userInfoFuture.thenCombine(productInfoFuture, (user, product) -> {
// 这里对获取到的用户信息和商品信息进行整合处理,比如拼接成一个字符串返回
return "用户:" + user.getName() + ",年龄:" + user.getAge() + ",购买商品:" + product.getName() + ",价格:" + product.getPrice();
});
// 获取最终合并后的结果,可以选择合适的结果获取方式,这里使用join方法(会阻塞当前线程直到任务完成)
String result = combinedFuture.join();
System.out.println(result);
在这段代码中:
- 首先发起了两个异步任务userInfoFuture和productInfoFuture,它们会并行去执行查询操作,各自耗时对应的时间(分别模拟为 1 秒和 1.5 秒)。
- 然后通过thenCombine方法,将这两个任务的结果进行合并,在合并的逻辑中,把用户信息和商品信息按照一定格式拼接成一个字符串。
- 最后使用join方法获取最终的结果,这里如果不使用join方法,也可以使用get方法等其他获取结果的方式,但要注意get方法会抛出检查异常需要进行处理,而join方法抛出的是未经检查的异常。
通过这种异步查询和结果合并的方式,相比于传统的先查询用户信息,等待完成后再去查询商品信息,然后再整合的串行方式,大大提高了效率。原本串行执行总共需要耗时大约 2.5 秒(1 秒 + 1.5 秒),而现在由于是并行执行,只需要大约 1.5 秒(取决于耗时较长的那个任务的时间)就可以完成整个查询和整合的操作,提升了程序的响应速度和性能。
(二)复杂业务流程场景
在实际开发中,经常会遇到复杂的业务流程,涉及多个服务调用,并且这些调用存在先后顺序和依赖关系。例如用户注册后,需要进行验证、权限分配、信息存储等多个步骤,下面我们就用 CompletableFuture 来对这样的复杂业务场景进行任务编排。
假设我们有以下几个模拟的服务方法,分别代表不同的业务操作:
import java.util.concurrent.CompletableFuture;
// 模拟用户验证服务,返回验证结果(这里简单返回true表示验证通过)
public CompletableFuture<Boolean> userValidation(String username, String password) {
return CompletableFuture.supplyAsync(() -> {
// 实际可以是调用验证接口,比对数据库等操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
});
}
// 模拟权限分配服务,根据用户信息分配权限
public CompletableFuture<String[]> assignPermissions(boolean isValidated) {
return CompletableFuture.supplyAsync(() -> {
if (isValidated) {
// 实际会根据不同规则分配权限,这里简单返回一些权限示例
String[] permissions = {"read", "write"};
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return permissions;
}
return new String[]{};
});
}
// 模拟信息存储服务,将用户信息和权限等存储起来
public CompletableFuture<Void> storeUserInfo(String username, String[] permissions) {
return CompletableFuture.runAsync(() -> {
// 实际可以是插入数据库等存储操作
System.out.println("存储用户:" + username + " 的权限信息:" + String.join(",", permissions));
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
现在我们要按照用户注册后的流程,先进行验证,验证通过后分配权限,最后存储用户信息,使用 CompletableFuture 进行任务编排的代码如下:
String username = "李四";
String password = "123456";
CompletableFuture<Boolean> validationFuture = userValidation(username, password);
CompletableFuture<String[]> permissionsFuture = validationFuture.thenApply(assignPermissions);
CompletableFuture<Void> storeFuture = permissionsFuture.thenAccept(permissions -> storeUserInfo(username, permissions));
// 等待所有任务完成,这里使用join方法阻塞主线程直到所有相关任务都执行完毕
storeFuture.join();
System.out.println("用户注册相关流程全部完成");
在上述代码中:
- 首先通过userValidation方法发起异步的用户验证任务,返回一个CompletableFuture,代表验证结果的异步任务。
- 然后利用thenApply方法,在验证结果返回(也就是验证任务完成)后,将验证结果作为参数传递给assignPermissions方法,发起权限分配的异步任务,这样就实现了验证通过后才进行权限分配的依赖关系,得到permissionsFuture。
- 接着通过thenAccept方法,在权限分配完成后(即permissionsFuture完成),获取分配的权限信息并传递给storeUserInfo方法进行信息存储的异步操作,得到storeFuture。
- 最后使用join方法等待整个流程的所有任务都执行完毕,确保用户注册相关的验证、权限分配和信息存储这一系列复杂业务流程按顺序且依赖地完成。
完整可运行的代码示例如下:
import java.util.concurrent.CompletableFuture;
public class ComplexBusinessExample {
// 模拟用户验证服务,返回验证结果(这里简单返回true表示验证通过)
public static CompletableFuture<Boolean> userValidation(String username, String password) {
return CompletableFuture.supplyAsync(() -> {
// 实际可以是调用验证接口,比对数据库等操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
});
}
// 模拟权限分配服务,根据用户信息分配权限
public static CompletableFuture<String[]> assignPermissions(boolean isValidated) {
return CompletableFuture.supplyAsync(() -> {
if (isValidated) {
// 实际会根据不同规则分配权限,这里简单返回一些权限示例
String[] permissions = {"read", "write"};
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return permissions;
}
return new String[]{};
});
}
// 模拟信息存储服务,将用户信息和权限等存储起来
public static CompletableFuture<Void> storeUserInfo(String username, String[] permissions) {
return CompletableFuture.runAsync(() -> {
// 实际可以是插入数据库等存储操作
System.out.println("存储用户:" + username + " 的权限信息:" + String.join(",", permissions));
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static void main(String[] args) {
String username = "李四";
String password = "123456";
CompletableFuture<Boolean> validationFuture = userValidation(username, password);
CompletableFuture<String[]> permissionsFuture = validationFuture.thenApply(ComplexBusinessExample::assignPermissions);
CompletableFuture<Void> storeFuture = permissionsFuture.thenAccept(permissions -> storeUserInfo(username, permissions));
// 等待所有任务完成,这里使用join方法阻塞主线程直到所有相关任务都执行完毕
storeFuture.join();
System.out.println("用户注册相关流程全部完成");
}
}
通过这样的任务编排,我们可以清晰地处理复杂业务流程中的各个步骤及其依赖关系,利用 CompletableFuture 的异步特性,提高整个业务流程的执行效率,避免了传统的串行方式中每个步骤等待上一步骤完成而造成的时间浪费,让程序在处理这类复杂场景时更加高效、易维护。
五、总结
(一)回顾重点
在本文中,我们深入探讨了 CompletableFuture 的诸多奇技淫巧及应用场景。
首先,介绍了 CompletableFuture 的创建方式,像常用的supplyAsync(用于创建有返回值的异步任务)和runAsync(创建无返回值的异步任务)方法,并且讲解了它们在使用默认线程池以及自定义线程池时的不同情况,让开发者能够根据实际业务对线程资源进行合理管理。
在结果获取方面,详细阐述了get、getNow、join、get(long timeout, TimeUnit unit)这四种方式,每种方式各有特点,适用于不同的业务场景需求,例如getNow适合不想长时间阻塞等待结果,希望先拿到默认值进行后续处理的场景,而get(long timeout, TimeUnit unit)则适用于对异步任务执行时间有预期,需避免无限制等待的情况。
异步回调方法也是重点内容之一,涵盖了thenRun/thenRunAsync、thenAccept、thenApply、thenCombine、thenCompose等方法。它们在任务完成后的执行逻辑、是否使用返回值、对多个任务结果的处理等方面各有差异,开发者可以灵活运用这些方法,实现对异步任务结果的多样化处理以及任务之间的组合、链式调用等复杂操作。
异常处理技巧部分,讲解了exceptionally和handle方法,通过这两个方法能在异步任务出现异常时,按照预期逻辑进行处理,避免程序中断或后续逻辑无法执行的情况,增强程序的健壮性。
还有多任务编排与执行方面,无论是任务的串行执行,通过链式调用清晰表达任务依赖关系,还是利用allOf和anyOf方法实现并行执行多个任务并根据不同需求进行后续处理,都展现了 CompletableFuture 在处理复杂业务流程时的强大能力。
最后,通过模拟查询场景和复杂业务流程场景这两个实际开发中的应用案例,展示了如何将 CompletableFuture 的各种功能应用到具体业务中,提升代码性能和开发效率,使其异步编程更加高效、简洁且易维护。掌握这些 CompletableFuture 的关键内容,对于提升在 Java 中的异步编程能力是非常重要的,能够帮助开发者更好地应对各种复杂的业务场景,编写出高质量的异步代码。
(二)鼓励实践
亲爱的读者,纸上得来终觉浅,希望大家能够将 CompletableFuture 运用到实际的开发项目中。在面对不同的业务需求时,充分考虑各功能特点,灵活选择合适的创建方式、结果获取方式以及异步回调方法等。例如,当你需要同时执行多个相互独立的任务,如批量发送邮件、记录多条日志等,可以使用allOf方法并行执行这些任务,待全部完成后再统一进行后续处理;若存在任务依赖关系,像用户注册流程中验证、权限分配、信息存储等先后步骤,则利用任务的串行执行方式,通过链式调用清晰展现依赖逻辑。
在处理可能出现异常的情况时,适时运用exceptionally或handle方法来保障程序的稳定性。同时,根据对资源隔离、性能等方面的要求,合理选用默认线程池或者自定义线程池来执行异步任务。总之,多在实际项目中尝试使用 CompletableFuture,相信你会感受到它为异步编程