CompletableFuture多线程并发处理
CompletableFuture多线程并发处理
概要
一个接口可能需要调用 N 个其他服务的接口,这在项目开发中还是挺常见的。
举个例子:用户请求获取订单信息,可能需要调用用户信息、商品详情、物流信息、商品推荐等接口,如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速度会非常慢。考虑到这些接口之间有大部分都是无前后顺序关联的,可以 并行执行 ,就比如说调用获取商品详情的时候,可以同时调用获取物流信息。通过并行执行多个任务的方式,接口的响应速度会得到大幅优化。
对于 Java 程序来说,Java 8 才被引入的 CompletableFuture 可以帮助我们来做多个任务的编排,功能非常强大。
一、 Future
Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。
在 Java 中,Future 类只是一个泛型接口,位于 java.util.concurrent 包下,其中定义了 5 个方法,主要包括下面这 4 个功能:
- 取消任务;
- 判断任务是否被取消;
- 判断任务是否已经执行完成;
- 获取任务执行结果。
1 // V 代表了Future执行的任务返回值的类型 2 public interface Future<V> { 3 // 取消任务执行 4 // 成功取消返回 true,否则返回 false 5 boolean cancel(boolean mayInterruptIfRunning); 6 // 判断任务是否被取消 7 boolean isCancelled(); 8 // 判断任务是否已经执行完成 9 boolean isDone(); 10 // 获取任务执行结果 11 V get() throws InterruptedException, ExecutionException; 12 // 指定时间内没有返回计算结果就抛出 TimeOutException 异常 13 V get(long timeout, TimeUnit unit) 14 15 throws InterruptedException, ExecutionException, TimeoutExceptio 16 17 }
简单理解就是:我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。
一段时间之后,我就可以 Future 那里直接取出任务执行结果。
二、 CompletableFuture介绍
Future 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。
虽然用CountDownLatch可以解决多个异步任务需要相互依赖的场景,但是在Java8以后我们不在认为这是一种优雅的解决方式,接下来来了解下CompletableFuture的使用。
Java 8 才被引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
CompletableFuture是高级的多线程功能,支持自定义线程池和系统默认的线程池,是多线程、高并发里面,经常需要用到的,比直接创建线程,要简单易用的方法。它能够简化异步任务的执行和结果处理。
如下图:
CompletableFuture 同时实现了 Future 和 CompletionStage 接口。
CompletionStage 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
三、CompletableFuture 常见操作
1. 创建CompletableFuture对象
常见的创建 CompletableFuture 对象的方法如下:
1)通过 new 关键字
2)基于 CompletableFuture 自带的静态工厂方法:runAsync()、supplyAsync()
runAsync()
runAsync()方法接受的参数是 Runnable ,这是一个函数式接口,不允许返回值。当你需要异步操作且不关心返回结果的时候可以使用 runAsync() 方法。
代码示例:
1 //runAsync 函数式接口 不允许返回值 2 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("hello!")); 3 4 future.get(); 5 6 //执行结果 7 hello!
supplyAsync()
supplyAsync()方法接受的参数是 Supplier<U> ,这也是一个函数式接口,U 是返回结果值的类型。需要异步操作且关心返回结果时 可以使用supplyAsync()方法。
代码示例:
1 //supplyAsync 需要异步操作且关心返回结果 2 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!"); 3 4 System.out.println(future.get()); 5 6 //执行结果 7 hello!
2. 处理异步计算的结果
当我们获取到异步计算的结果之后,还可以对其进行进一步的处理,比较常用的方法有下面几个:
- thenApply()
- thenAccept()
- thenRun()
- whenComplete()
1)thenApply() 方法接受一个 Function 实例,用它来处理结果。
1 CompletableFuture<String> future = CompletableFuture.completedFuture("hello!") 2 .thenApply(s -> s + "world!"); 3 assertEquals("hello!world!", future.get()); 4 // 这次调用将被忽略。 5 future.thenApply(s -> s + "nice!"); 6 assertEquals("hello!world!", future.get());
2) thenAccept 以及 thenRun
不需要从回调函数中获取返回结果,可以使用 thenAccept() 或者 thenRun()。这两个方法的区别在于 thenRun() 不能访问异步计算的结果。
通俗点讲就是,做完第一个任务后,再做第二个任务,第二个任务也没有返回值。
3) thenRun 和thenRunAsync有什么区别呢?
如果你执行第一个任务的时候,传入了一个自定义线程池。
调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池。
说明: 后面介绍的thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是这个。
4)thenAccept/thenAcceptAsync
第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,但是回调方法是没有返回值的。
5) whenComplete
whenComplete() 的方法的参数是 BiConsumer<? super T, ? super Throwable> 。相对于 Consumer , BiConsumer 可以接收 2 个输入对象然后进行“消费”。
当CompletableFuture的任务不论是正常完成还是出现异常它都会调用「whenComplete」这回调函数。
正常完成:whenComplete返回结果和上级任务一致,异常为null;
出现异常:whenComplete返回结果为null,异常为上级任务的异常;
即调用get()时,正常完成时就获取到结果,出现异常时就会抛出异常,需要你处理该异常。
代码示例:
1 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!") 2 .whenComplete((res, ex) -> { 3 // res 代表返回的结果 4 // ex 的类型为 Throwable ,代表抛出的异常 5 System.out.println(res); 6 // 这里没有抛出异常所以为 null 7 assertNull(ex); 8 }); 9 10 System.out.println(future.get()); 11 12 //执行结果 13 hello!
3. 异常处理
1) handle()
handle() 方法用来处理任务执行过程中可能出现的抛出异常的情况。
1 CompletableFuture<String> future 2 = CompletableFuture.supplyAsync(() -> { 3 if (true) { 4 throw new RuntimeException("Computation error!"); 5 } 6 return "hello!"; 7 }).handle((res, ex) -> { 8 // res 代表返回的结果 9 // ex 的类型为 Throwable ,代表抛出的异常 10 return res != null ? res : "world!"; 11 }); 12 System.out.println(future.get()); 13 14 //执行结果 15 world!
2) exceptionally()
exceptionally() 方法来处理异常情况。
1 CompletableFuture<String> future 2 = CompletableFuture.supplyAsync(() -> { 3 if (true) { 4 throw new RuntimeException("Computation error!"); 5 } 6 return "hello!"; 7 }).exceptionally(ex -> { 8 // CompletionException 9 System.out.println("exceptionally:" + ex.toString()); 10 return "world!"; 11 }); 12 System.out.println(future.get()); 13 14 //执行结果 15 exceptionally:java.util.concurrent.CompletionException: java.lang.RuntimeException: Computation error! 16 world!
3) completeExceptionally()
如果想让 CompletableFuture 的结果就是异常的话,可以使用 completeExceptionally() 方法为其赋值。
1 CompletableFuture<String> completableFuture = new CompletableFuture<>(); 2 3 //让CompletableFuture 的结果就是异常 4 completableFuture.completeExceptionally(new RuntimeException("Calculation failed!")); 5 6 // ExecutionException 7 completableFuture.get();
执行结果:
4. 组合CompletableFuture
1) thenCompose
任务之间有先后依赖顺序。
可以使用 thenCompose() 按顺序链接两个 CompletableFuture 对象,实现异步的任务链。它的作用是将前一个任务的返回结果作为下一个任务的输入参数,从而形成一个依赖关系。
1 //thenCompose() 按顺序链接两个 CompletableFuture 对象,实现异步的任务链。将前一个任务的返回结果作为下一个任务的输入参数 2 CompletableFuture<String> future 3 = CompletableFuture.supplyAsync(() -> "hello!") 4 .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!")); 5 6 System.out.println(future.get()); 7 8 //执行结果 9 hello!world!
2) thenCombine
任务之间没有先后依赖顺序。
thenCombine会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。
代码示例如下:
1 //thenCombine() 会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。 2 CompletableFuture<String> future 3 = CompletableFuture.supplyAsync(() -> "hello!") 4 .thenCombine(CompletableFuture.supplyAsync( 5 () -> "world!"), (s1, s2) -> s1 + s2) 6 .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "nice!")); 7 8 System.out.println(future.get()); 9 10 //执行结果 11 hello!world!nice!
3) acceptEither
如果我们想要实现 task1 和 task2 中的任意一个任务执行完后就执行 task3 的话,可以使用 acceptEither()。
代码示例如下:
1 //如果我们想要实现 task1 和 task2 中的任意一个任务执行完后就执行 task3 的话,可以使用 acceptEither() 2 CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> { 3 System.out.println("任务1开始执行,当前时间:" + System.currentTimeMillis()); 4 try { 5 Thread.sleep(500); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 System.out.println("任务1执行完毕,当前时间:" + System.currentTimeMillis()); 10 return "task1"; 11 }); 12 13 CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> { 14 System.out.println("任务2开始执行,当前时间:" + System.currentTimeMillis()); 15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 System.out.println("任务2执行完毕,当前时间:" + System.currentTimeMillis()); 21 return "task2"; 22 }); 23 24 task1.acceptEitherAsync(task2, (res) -> { 25 System.out.println("任务3开始执行,当前时间:" + System.currentTimeMillis()); 26 System.out.println("上一个任务的结果为:" + res); 27 }); 28 29 // 增加一些延迟时间,确保异步任务有足够的时间完成 30 try { 31 Thread.sleep(2000); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 36 //执行结果 37 任务1开始执行,当前时间:1718932501604 38 任务2开始执行,当前时间:1718932501604 39 任务1执行完毕,当前时间:1718932502109 40 任务3开始执行,当前时间:1718932502109 41 上一个任务的结果为:task1 42 任务2执行完毕,当前时间:1718932502609
5. 并行运行多个CompletableFuture
我们可以通过 CompletableFuture 的 allOf()这个静态方法来并行运行多个 CompletableFuture 。实际项目中,我们经常需要并行运行多个互不相关的任务,这些任务之间没有依赖关系,可以互相独立地运行。
比说我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。像这种情况我们就可以使用并行运行多个 CompletableFuture 来处理。
1)allOf
allOf() 方法会等到所有的 CompletableFuture 都运行完成之后再返回。
说明:调用 join() 可以让程序等future1 和 future2 都运行完了之后再继续执行。
代码示例:
1 Random rand = new Random(); 2 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { 3 try { 4 Thread.sleep(1000 + rand.nextInt(1000)); 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } finally { 8 System.out.println("future10 done..."); 9 } 10 return "abc"; 11 }); 12 CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { 13 try { 14 Thread.sleep(1000 + rand.nextInt(1000)); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } finally { 18 System.out.println("future11 done..."); 19 } 20 return "efg"; 21 }); 22 23 //allOf() 方法会等到所有的 CompletableFuture 都运行完成之后再返回 24 CompletableFuture<Void> future3 = CompletableFuture.allOf(future1, future2); 25 //调用 join() 等待所有的CompletableFuture完成 26 future3.join(); 27 System.out.println("all futures done..."); 28 String res1 = future1.join(); 29 String res2 = future2.join(); 30 System.out.println(res1 + res2); 31 32 //执行结果 33 future2 done... 34 future1 done... 35 all futures done... 36 abcefg
2)anyOf
anyOf() 方法不会等待所有的 CompletableFuture 都运行完成之后再返回,只要有一个执行完成即可。
代码示例:
1 Random rand = new Random(); 2 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { 3 try { 4 Thread.sleep(1000 + rand.nextInt(1000)); 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } finally { 8 System.out.println("future10 done..."); 9 } 10 return "abc"; 11 }); 12 CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { 13 try { 14 Thread.sleep(1000 + rand.nextInt(1000)); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } finally { 18 System.out.println("future11 done..."); 19 } 20 return "efg"; 21 }); 22 23 //anyOf() 方法不会等待所有的 CompletableFuture 都运行完成之后再返回,只要有一个执行完成即可! 24 CompletableFuture<Object> f = CompletableFuture.anyOf(future1, future2); 25 System.out.println(f.get()); 26 27 //执行结果 28 abc
三、编排任务的时候,依赖的服务可能出现异常的情况处理
举个例子:现在将A、B、C、D这四个任务做编排组合,其中A依赖于B,其结果与C、D并行。关系如下图:
因为A任务依赖于B任务,所以先执行B任务,再执行A任务,其执行结果与C、D并行
代码示例如下:
1 public class TaskExample { 2 public static void main(String[] args) throws ExecutionException, InterruptedException { 3 // 模拟任务B,可能抛出异常 4 CompletableFuture<Integer> taskB = CompletableFuture.supplyAsync(() -> { 5 System.out.println("Task B is running..."); 6 if (Math.random() > 0.5) { 7 throw new RuntimeException("Task B failed"); // 模拟异常 8 } 9 return 10; // 成功返回值 10 }); 11 12 // 任务A依赖任务B的结果,并处理B的异常(将前一个任务的返回结果作为下一个任务的输入参数) 13 CompletableFuture<Integer> taskA = taskB.thenCompose(bResult -> { 14 // 如果B成功,A依赖B的结果进行计算 15 System.out.println("Task A is running with result from B: " + bResult); 16 return CompletableFuture.supplyAsync(() -> bResult * 2); 17 }).exceptionally(ex -> { 18 // 如果任务B失败,执行备用逻辑 19 System.out.println("Task B failed, fallback for Task A."); 20 return -1; // 备用值 21 }); 22 23 // 任务C和任务D并行执行,不依赖于任务A或B 24 CompletableFuture<Integer> taskC = CompletableFuture.supplyAsync(() -> 5); 25 26 CompletableFuture<Integer> taskD = CompletableFuture.supplyAsync(() -> 6); 27 28 29 // 使用allOf等待任务A、C、D都完成,并在所有任务完成后执行后续操作 30 CompletableFuture<Void> allTasks = CompletableFuture.allOf(taskA, taskC, taskD) 31 .handle((v, ex) -> { 32 if (ex != null) { 33 // 统一处理所有任务的异常 34 System.out.println("An error occurred in one of the tasks: " + ex.getMessage()); 35 } 36 return null; 37 }); 38 39 // 等待A、C、D都完成 40 allTasks.join(); 41 Integer resA = taskA.join(); 42 Integer resC = taskC.join(); 43 Integer resD = taskD.join(); 44 45 // 获取任务A的结果 46 System.out.println("Task A result: " + resA); 47 // 获取任务C的结果 48 System.out.println("Task C result: " + resC); 49 // 获取任务D的结果 50 System.out.println("Task D result: " + resD); 51 int res = resA + resC + resD; 52 53 System.out.println("总和:" + res); 54 } 55 }
四、CompletableFuture 使用建议
1. 使用自定义线程池
CompletableFuture 默认使用ForkJoinPool.commonPool() 作为执行器,这个线程池是全局共享的,可能会被其他任务占用,导致性能下降或者饥饿。
因此,建议使用自定义的线程池来执行 CompletableFuture 的异步任务,可以提高并发度和灵活性。
2. 尽量避免使用get
CompletableFuture的get()方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。
3. 正确进行异常处理
使用 CompletableFuture的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。
1)使用 whenComplete 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。
2)使用 exceptionally 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。
3)使用 handle 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。
4)使用 CompletableFuture.allOf 方法可以组合多个 CompletableFuture,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。
4. 合理组合多个异步任务
正确使用 thenCompose() 、 thenCombine() 、acceptEither()、allOf()、anyOf()等方法来组合多个异步任务,以满足实际业务的需求,提高程序执行效率。
参考链接:
https://javaguide.cn/java/concurrent/completablefuture-intro.html