CompletableFuture的简单使用
日常开发中,我们都会用到线程池,一般会用execute()和submit()方法提交任务。但是当你用过CompletableFuture之后,就会发现以前的线程池处理任务有多难用,功能有多简陋,CompletableFuture又是多么简洁优雅。
要知道CompletableFuture已经随着Java8发布7年了,还没有过它就有点说不过去了。
一、线程池处理任务和CompletableFuture处理任务
1.1、使用线程池处理任务
1 /** 2 * 1. 使用线程池处理任务 3 */ 4 @Test 5 public void test1() throws ExecutionException, InterruptedException { 6 // 1. 创建线程池 7 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); 8 List<Integer> list = Arrays.asList(1, 2, 3); 9 10 for (Integer key : list) { 11 // 2. 提交任务 12 Future<String> future = executorService.submit(() -> { 13 // 睡眠一秒,模仿处理过程 14 Thread.sleep(1000L); 15 return "结果" + key; 16 }); 17 // 3. 获取结果 18 System.out.println(future.get()); 19 } 20 executorService.shutdown(); 21 }
输出结果:
结果1 结果2 结果3
这里能够发现两个问题:
- 获取结果时,调用的future.get()方法,会阻塞当前线程,直到返回结果,大大降低性能
- 有一半的代码在写怎么使用线程,其实我们不应该关心怎么使用线程,更应该关注任务的处理
有没有具体的优化方案呢?当然有了,请出来我们今天的主角CompletableFuture
1.2、使用CompletableFuture处理任务
1 /** 2 * 使用CompletableFuture重构任务处理 3 */ 4 @Test 5 public void test2() { 6 // 1. 创建线程池 7 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); 8 9 List<Integer> list = Arrays.asList(1, 2, 3); 10 for (Integer key : list) { 11 // 2. 提交任务 12 CompletableFuture.supplyAsync(() -> { 13 // 睡眠一秒,模仿处理过程 14 try { 15 Thread.sleep(1000L); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 return "结果" + key; 20 }, executorService).whenCompleteAsync((result, exception) -> { 21 // 3. 获取结果 22 System.out.println(result); 23 }); 24 } 25 26 executorService.shutdown(); 27 // 由于whenCompleteAsync获取结果的方法是异步的,所以要阻塞当前线程才能输出结果 28 try { 29 Thread.sleep(2000L); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 }
输出结果:
结果2 结果3 结果1
可以看到是ComparableFutrue是通过supplyAsync方法进行异步执行任务的。
=============================================================
可是上面的任务执行还是有一些问题的,需要阻塞当前线程输出结果,但是我们并不能确定线程的执行结束时间。也就无法准确设置线程阻塞时间,这样子就会导致最后执行的结果不对。
我们可以通过join进行优化一下:异步任务完成时统一返回结果值
1 public void test2() { 2 // 1. 创建线程池 3 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); 4 5 List<Integer> list = Arrays.asList(1, 2, 3); 6 List<String> joinResult = new ArrayList<>(); 7 for (Integer key : list) { 8 // 2. 提交任务 9 String taskResult = CompletableFuture.supplyAsync(() -> { 10 // 睡眠一秒,模仿处理过程 11 try { 12 Thread.sleep(1000L); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 return "结果" + key; 17 // 完成时返回结果值 18 }, executorService).join(); 19 joinResult.add(taskResult); 20 } 21 executorService.shutdown(); 22 // 3、打印结果 23 joinResult.forEach(System.out::println); 24 }
代码中使用了CompletableFuture的两个方法:
supplyAsync()方法作用是提交异步任务,有两个传参,任务和自定义线程池。
whenCompleteAsync()方法作用是异步获取结果,也有两个传参,结果和异常信息。
代码经过CompletableFuture改造后,是多么的简洁优雅。
提交任务也不用再关心线程池是怎么使用了,获取结果也不用再阻塞当前线程了。
==============================================================
又或者可以通过Java8的Stream流进进一步简化test2的代码:
/** * 获取同步的结果 */ @Test public void test3() { // 1. 创建线程池 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); List<Integer> list = Arrays.asList(1, 2, 3); // 2. 提交任务 List<String> results = list.stream().map(key -> CompletableFuture.supplyAsync(() -> { // 睡眠一秒,模仿处理过程 try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } return "结果" + key; }, executorService)) .map(CompletableFuture::join).collect(Collectors.toList()); executorService.shutdown(); // 3. 获取结果 System.out.println(results); }
二、CompletableFuture更多使用
2.1、等待所有任务执行完成
第一种:使用CountDownLatch方式
/** * CountDownLatch-等待所有任务执行完毕 */ @Test public void test4() { long beginTime = System.currentTimeMillis(); // 1. 创建线程池 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); List<Integer> list = Arrays.asList(1, 2, 3); CountDownLatch countDownLatch = new CountDownLatch(list.size()); for (Integer key : list) { // 2. 提交任务 executorService.submit(() -> { // 睡眠一秒,模仿处理过程 try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结果" + key); countDownLatch.countDown(); }); } executorService.shutdown(); try { // 3. 阻塞等待所有任务执行完成 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis() - beginTime + "ms"); }
第二种:使用CompletableFuture
1 @Test 2 public void test5() { 3 long beginTime = System.currentTimeMillis(); 4 // 1. 创建线程池 5 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); 6 List<Integer> list = Arrays.asList(1, 2, 3); 7 List<String> resultList = new CopyOnWriteArrayList<>(); 8 9 // 2. 任务数组 10 CompletableFuture[] completableFutures = list.stream().map(key -> 11 CompletableFuture.runAsync(() -> { 12 // 睡眠一秒,模仿处理过程 13 try { 14 Thread.sleep(1000L); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 resultList.add("结果" + key); 19 }, executorService)) 20 .toArray(CompletableFuture[]::new); 21 22 // 通过allOf执行异步任务,里面是一个completableFuture数组。并调用join()阻塞等待所有任务执行完成。 23 CompletableFuture.allOf(completableFutures).join(); 24 executorService.shutdown(); 25 // 打印结果 26 resultList.forEach(System.out::println); 27 System.out.println(System.currentTimeMillis() - beginTime + "ms"); 28 }
打印结果:
结果2 结果1 结果3 1003ms
这里需要说明的一点是,我这边使用了CopyOnWriteArrayList,因为是在多线程环境下,如果使用ArrayList会导致有数据无法成功添加到集合中。
代码看着有点乱,其实逻辑很清晰。
- 遍历list集合,提交CompletableFuture任务,把结果转换成数组
- 再把数组放到CompletableFuture的allOf()方法里面
- 最后调用join()方法阻塞等待所有任务执行完成
CompletableFuture的allOf()方法的作用就是,等待所有任务处理完成。
2.2、任何一个任务处理完成就返回
如果要实现这样一个需求,往线程池提交一批任务,只要有其中一个任务处理完成就返回。
该怎么做?如果你手动实现这个逻辑的话,代码肯定复杂且低效,有了CompletableFuture就非常简单了,只需调用anyOf()方法就行了
1 @Test 2 public void test6() { 3 long beginTime = System.currentTimeMillis(); 4 // 1. 创建线程池 5 ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); 6 List<Integer> list = Arrays.asList(1, 2, 3); 7 8 // 2. 提交任务 9 CompletableFuture<Object> completableFuture = CompletableFuture 10 .anyOf( 11 list.stream().map(key -> 12 CompletableFuture.supplyAsync(() -> { 13 // 睡眠一秒,模仿处理过程 14 try { 15 Thread.sleep(1000L); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 return "结果" + key; 20 }, executorService)) 21 .toArray(CompletableFuture[]::new)); 22 23 executorService.shutdown(); 24 25 // 3. 获取结果 26 System.out.println(completableFuture.join()); 27 System.out.println(System.currentTimeMillis() - beginTime + "ms"); 28 }
执行结果:
结果1 1007ms
2.3、 一个线程执行完成,交给另一个线程接着执行
1 @Test 2 public void test7() { 3 // 1. 创建线程池 4 ExecutorService executorService = Executors.newFixedThreadPool(2); 5 List<String> resultList = new ArrayList<>(); 6 // 2. 提交任务,并调用join()阻塞等待任务执行完成 7 String result2 = CompletableFuture.supplyAsync(() -> { 8 // 睡眠一秒,模仿处理过程 9 try { 10 Thread.sleep(1000L); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 return "结果1"; 15 // 拿着任务1返回的结果来用 16 }, executorService).thenApplyAsync(result1 -> { 17 // 睡眠一秒,模仿处理过程 18 try { 19 Thread.sleep(1000L); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 return result1 + "结果2"; 24 }, executorService).join(); 25 26 executorService.shutdown(); 27 // 3. 获取结果 28 System.out.println(result2); 29 }
执行结果:
结果1结果2
代码主要用到了CompletableFuture的thenApplyAsync()方法,作用就是异步处理上一个线程的结果。
三、CompletableFuture常用API
3.1、常用API规则
提交任务:
- supplyAsync
- runAsync
接力处理:
- thenRun thenRunAsync
- thenAccept thenAcceptAsync
- thenApply thenApplyAsync
- handle handleAsync
- applyToEither applyToEitherAsync
- acceptEither acceptEitherAsync
- runAfterEither runAfterEitherAsync
- thenCombine thenCombineAsync
- thenAcceptBoth thenAcceptBothAsync
获取结果:
- join 阻塞等待,不会抛异常
- get 阻塞等待,会抛异常
- complete(T value) 不阻塞,如果任务已完成,返回处理结果。如果没完成,则返回传参value。
- completeExceptionally(Throwable ex) 不阻塞,如果任务已完成,返回处理结果。如果没完成,抛异常。
总结分类
带run的方法,无入参,无返回值。
带accept的方法,有入参,无返回值。
带supply的方法,无入参,有返回值。
带apply的方法,有入参,有返回值。
带handle的方法,有入参,有返回值,并且带异常处理。
以Async结尾的方法,都是异步的,否则是同步的。
以Either结尾的方法,只需完成任意一个。
以Both/Combine结尾的方法,必须所有都完成。
3.2、 then,handle方法使用示例
@Test public void test8() { CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> { System.out.println("1. 开始淘米"); return "2. 淘米完成"; }).thenApplyAsync(result -> { System.out.println(result); System.out.println("3. 开始煮饭"); // 生成一个0~10的随机数 if (new Random().nextInt(11) > 5) { throw new RuntimeException("4. 电饭煲坏了,煮不了"); } return "4. 煮饭完成"; }).handleAsync((result, exception) -> { if (exception != null) { System.out.println(exception.getMessage()); return "5. 今天没饭吃"; } else { System.out.println(result); return "5. 开始吃饭"; } }); try { String result = completableFuture.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } }
输出结果可能是:
1. 开始淘米 2. 淘米完成 3. 开始煮饭 4. 煮饭完成 5. 开始吃饭
也可能是:
1. 开始淘米 2. 淘米完成 3. 开始煮饭 java.lang.RuntimeException: 4. 电饭煲坏了,煮不了 5. 今天没饭吃
3.3、complete方法使用示例
1 @Test 2 public void test9() { 3 CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "饭做好了"); 4 5 // try { 6 // Thread.sleep(1L); 7 // } catch (InterruptedException e) { 8 // e.printStackTrace(); 9 // } 10 11 completableFuture.complete("饭还没做好,我点外卖了"); 12 System.out.println(completableFuture.join()); 13 }
打印结果是:
饭还没做好,我点外卖了
如果去掉sleep语句,那么打印的就是:
饭做好了
3.4、either方法使用示例
1 @Test 2 public void test10() { 3 CompletableFuture<String> meal = CompletableFuture.supplyAsync(() -> "饭做好了"); 4 CompletableFuture<String> outMeal = CompletableFuture.supplyAsync(() -> "外卖到了"); 5 6 // 饭先做好,就吃饭。外卖先到,就吃外卖。就是这么任性。 7 CompletableFuture<String> completableFuture = meal.applyToEither(outMeal, myMeal -> myMeal); 8 9 System.out.println(completableFuture.join()); 10 }
输出结果可能是:‘饭做好了’,也有可能是‘外卖到了’