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

这里能够发现两个问题:

  1. 获取结果时,调用的future.get()方法,会阻塞当前线程,直到返回结果,大大降低性能
  2. 有一半的代码在写怎么使用线程,其实我们不应该关心怎么使用线程,更应该关注任务的处理

有没有具体的优化方案呢?当然有了,请出来我们今天的主角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会导致有数据无法成功添加到集合中。

代码看着有点乱,其实逻辑很清晰。

  1. 遍历list集合,提交CompletableFuture任务,把结果转换成数组
  2. 再把数组放到CompletableFuture的allOf()方法里面
  3. 最后调用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     }

输出结果可能是:‘饭做好了’,也有可能是‘外卖到了’

posted @ 2022-08-25 17:11  Java小白的搬砖路  阅读(614)  评论(0编辑  收藏  举报