多线程任务编排:CompletableFuture从入门到精通

1:背景

我们把Runnable理解为最基本的线程任务,只具备在线程下执行一段逻辑的能力。为了获取执行的返回值,创造了Callable和与其配合使用的Future。future 通过提交一个 callable 任务给线程池,线程池后台启动其他线程去执行,然后再调用 get() 方法获取结果

private void test() {
  ExecutorService executor = Executors.newCachedThreadPool();
  Future<Integer> future = executor.submit(() -> sleep(1));
  try {
    Integer integer = future.get(3, TimeUnit.SECONDS);
    System.out.println(integer);
  } catch (InterruptedException e) {
    // 当前线在等待中被中断
    e.printStackTrace();
  } catch (ExecutionException e) {
    // 任务执行中的异常
    e.printStackTrace();
  } catch (TimeoutException e) {
    // 超时
    e.printStackTrace();
  }
}

private int sleep(int timeout) {
  try {
    TimeUnit.SECONDS.sleep(timeout);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  return 1;
}

为了将任务之间进行逻辑编排,就诞生了CompletableFuture。关于如何理解任务的逻辑编排,举一个简单的例子:

打开电脑-更新系统这两个操作是有先后顺序的,但是泡茶和这两个操作没有先后顺序,是可以并行的,而开始办公必须要等待其他操作结束之后才能进行,这就形成了任务编排的执行链。

在IO密集型系统中,类似的场景有很多。因为不同数据集的查询依赖主键不同,A数据集的查询主键是B数据集的一个字段这种情况很常见,通常还需要并发查询多个数据集的数据,所以对于多线程的执行编排是有需求的。

一种解决办法是CountDownLatch,让线程执行到某个地方后进行等待,直到依赖的任务执行结束。对于一些简单的执行链是可以满足的,但是当编排逻辑复杂起来,CountDownLatch会导致代码难以维护和调试。所以诞生了CompletableFuture用来描述和维护任务之间的依赖关系以进行任务编排。

2:初步了解使用方式

2.1:创建与执行

同步方法:

和FutureTask类似,CompletableFuture也通过get()方法获取执行结果。但是不同的是,CompletableFuture本身可以不承载可执行的任务(相比FutureTask则必须承载一个可执行的任务Callable),通过一个用于标记执行成功并设置返回值的函数,在使用上也更为灵活,如下:

CompletableFuture<String> demo = new CompletableFuture<>();
demo.complete("success");
1System.out.println(demo.get());
执行结果:success

和Future类似,get()函数也是同步阻塞的,调用get函数后线程会阻塞直到调用complete方法标记任务已经执行成功。

除了手动触发任务的完成,也可以让创建对象的同时就标记任务完成:

CompletableFuture<String> demo = CompletableFuture.completedFuture("success");
System.out.println(demo.get());
执行结果:success

异步方法:

相比于同步方法,异步执行更为常见。比如下面这个例子:

CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
    System.out.println("do something by thread" + Thread.currentThread().getName());
      return "success";
  });
    System.out.println(demo.get());
执行结果:
do something by threadForkJoinPool.commonPool-worker-9
success

supplyAsync方法接收一个Supplier对象,逻辑函数交给线程池中的线程异步执行

默认会使用ForkJoinPool的公共线程池来执行代码(不推荐),当然也可以指定线程池,如下:

ExecutorService executor = Executors.newFixedThreadPool(4);
  CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
      System.out.println("do something by thread" + Thread.currentThread().getName());
      return "success";
  }, executor);
  System.out.println(demo.get());
执行结果:
do something by threadpool-1-thread-1
success

如果不需要执行结果,也可以用runAsync方法:

	CompletableFuture.runAsync(() -> {
        System.out.println("do something by thread" + Thread.currentThread().getName());
    });
执行结果:
do something by threadForkJoinPool.commonPool-worker-9

2.2:多任务编排

多任务编排是CompletableFuture的核心,这里列举不同的场景来进行说明

一元依赖

步骤2需要依赖步骤1执行完毕才能执行,类似主线程的顺序执行,可以通过以下方式实现:

      ExecutorService executor = Executors.newFixedThreadPool(4);
      CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("执行【步骤1】");
        return "【步骤1的执行结果】";
      }, executor);

      CompletableFuture<String> step2 = step1.thenApply(result -> {
        System.out.println("上一步操作结果为:" + result);
        return "【步骤2的执行结果】";
      });
      System.out.println("步骤2的执行结果:" + step2.get());
执行结果:
执行【步骤1】
上一步操作结果为:【步骤1的执行结果】
步骤2的执行结果:【步骤2的执行结果】

通过thenApply方法接收上一个CompletableFuture对象的返回值,其中隐含的逻辑是,该处逻辑只有等上一个CompletableFuture对象执行完后才会执行

二元依赖

相比于一元依赖的顺序执行链,二元依赖更为常见,比如下面这个场景:

步骤1和2是并行的,而步骤3需要等步骤1和2执行完之后才能执行,通过CompletableFuture是这么实现的:

        ExecutorService executor = Executors.newFixedThreadPool(4);
        CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行【步骤1】");
            return "【步骤1的执行结果】";
        }, executor);

        CompletableFuture<String> step2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行【步骤2】");
            return "【步骤2的执行结果】";
        }, executor);

        CompletableFuture<String> step3 = step1.thenCombine(step2, (result1, result2) -> {
            System.out.println("前两步操作结果分别为:" + result1 + result2);
            return "【步骤3的执行结果】";
        });
        
        System.out.println("步骤3的执行结果:" + step3.get());
执行结果:
执行【步骤1】
执行【步骤2】
前两步操作结果分别为:【步骤1的执行结果】【步骤2的执行结果】
步骤3的执行结果:【步骤3的执行结果】

通过thenCombine方法等待step1和step2都执行完毕后,获取其返回结果并执行一段新的逻辑

多元依赖

当然还可能有下面这种场景,步骤M需要依赖1-N的N个前置节点:

这种情况会更为复杂,实现方式如下:

    	ExecutorService executor = Executors.newFixedThreadPool(4);
        CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行【步骤1】");
            return "【步骤1的执行结果】";
        }, executor);
        CompletableFuture<String> step2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行【步骤2】");
            return "【步骤2的执行结果】";
        }, executor);
        CompletableFuture<String> step3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行【步骤3】");
            return "【步骤3的执行结果】";
        }, executor);

        CompletableFuture<Void> stepM = CompletableFuture.allOf(step1, step2, step3);
        CompletableFuture<String> stepMResult = stepM.thenApply(res -> {
           // 通过join函数获取返回值
           String result1 = step1.join();
           String result2 = step2.join();
           String result3 = step3.join();
        
           return result1 + result2 + result3;
        });
        System.out.println("步骤M的结果:" + stepMResult.get());
执行结果:
执行【步骤1】
执行【步骤2】
执行【步骤3】
步骤M的结果:【步骤1的执行结果】【步骤2的执行结果】【步骤3的执行结果】

通过allOf函数声明当参数中的所有任务执行完毕后,才会执行下一步操作,但是要注意,allOf本身只是定义节点,真正阻塞的位置是thenApply函数。

和之前的方式不同,由于采用了不定变量,所以要通过CompletableFuture#join来获取每个任务的返回值。

除了allOf之外,如果我们需要任意一个任务完成后就执行下一步操作,可以使用anyOf方法,如下:

    // step1/2/3的定义相同
    // ...
    CompletableFuture<Object> stepM = CompletableFuture.anyOf(step1, step2, step3);
    System.out.println("步骤M的结果:" + stepM.get());
执行结果:
步骤M的结果:【步骤1的执行结果】

与allOf不同,anyOf的返回值即为第一个执行完毕的任务

3:工作原理

3.1:概念

在讲原理之前,先来了解一下CompletableFuture的定义。

CompletableFuture是Java 8 中新增的一个类,它是对Future接口的扩展。从下方的类继承关系图中我们看到其不仅实现了Future接口,还有CompletionStage接口,当Future需要显示地完成时,可以使用CompletionStage接口去支持完成时触发的函数和操作,当2个以上线程同时尝试完成、异常完成、取消一个CompletableFuture时,只有一个能成功。

CompletableFuture主要作用就是简化我们异步编程的复杂性,支持函数式编程,可以通过回调的方式处理计算结果。

3.2:为什么会有CompletableFuture 

在java5中,JDK为我们提供了Callable和Future,使我们可以很容易的完成异步任务结果的获取,但是通过Future的get获取异步任务结果会导致主线程的阻塞,这样在某些场景下是非常消耗CPU资源的,进而Java8为我们提供了CompletableFuture,使我们无需阻塞等待,而是通过回调的方式去处理结果,并且还支持流式处理、组合异步任务等操作。

3.3:下面我们就CompletableFuture 的使用进行简单分类:

  • 创建任务
    • supplyAsync/runAsync
  • 异步回调
    • thenApply/thenAccept/thenRun
    • thenApplyAsync/thenAcceptAsync/thenRunAsync
    • exceptionally
    • handle/whenComplete
  • 组合处理
    • thenCombine / thenAcceptBoth / runAfterBoth
    • applyToEither / acceptEither / runAfterEither
    • thenCompose
    • allOf / anyOf

具体内容请参照以下案例:

    public static void main(String[] args) throws Exception {
        // 1.带返回值的异步任务(不指定线程池,默认ForkJoinPool.commonPool(),单核ThreadPerTaskExecutor)
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            return 1 + 1;
        });
        System.out.println("cf1 result: " + cf1.get());
        // 2.无返回值的异步任务(不指定线程池,默认ForkJoinPool.commonPool(),单核ThreadPerTaskExecutor)
        CompletableFuture cf2 = CompletableFuture.runAsync(() -> {
            int a = 1 + 1;
        });
        System.out.println("cf2 result: " + cf2.get());
        // 3.指定线程池的带返回值的异步任务,runAsync同理
        CompletableFuture<Integer> cf3 = CompletableFuture.supplyAsync(() -> {
            return 1 + 1;
        }, Executors.newCachedThreadPool());
        System.out.println("cf3 result: " + cf3.get());
        // 4.回调,任务执行完成后执行的动作
        CompletableFuture<Integer> cf4 = cf1.thenApply((result) -> {
            System.out.println("cf4回调拿到cf1的结果 result : " + result);
            return result + 1;
        });
        System.out.println("cf4 result: " + cf4.get());
        // 5.异步回调(将回调任务提交到线程池),任务执行完成后执行的动作后异步执行
        CompletableFuture<Integer> cf5 = cf1.thenApplyAsync((result) -> {
            System.out.println("cf5回调拿到cf1的结果 result : " + result);
            return result + 1;
        });
        System.out.println("cf5 result: " + cf5.get());
        // 6.回调(同thenApply但无返回结果),任务执行完成后执行的动作
        CompletableFuture cf6 = cf1.thenAccept((result) -> {
            System.out.println("cf6回调拿到cf1的结果 result : " + result);
        });
        System.out.println("cf6 result: " + cf6.get());
        // 7.回调(同thenAccept但无入参),任务执行完成后执行的动作
        CompletableFuture cf7 = cf1.thenRun(() -> {
        });
        System.out.println("cf7 result: " + cf7.get());
        // 8.异常回调,任务执行出现异常后执行的动作
        CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("出现异常");
        });
        CompletableFuture<Integer> cf8 = cf.exceptionally((result) -> {
            return -1;
        });
        System.out.println("cf8 result: " + cf8.get());
        // 9.当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法
        //   如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致;
        //   如果该任务正常执行,则get方法返回执行结果,如果是执行异常,则get方法抛出异常。
        CompletableFuture<Integer> cf9 = cf1.handle((a, b) -> {
            if (b != null) {
                b.printStackTrace();
            }
            return a;
        });
        System.out.println("cf9 result: " + cf9.get());
        // 10 与handle类似,无返回值
        try {
            CompletableFuture<Integer> cf10 = cf.whenComplete((a, b) -> {
                if (b != null) {
                    b.printStackTrace();
                }
            });
            System.out.println("cf10 result: " + cf10.get());
        } catch (Exception e) {
            System.out.println("cf10 出现异常!!!");
        }
        // 11 组合处理(两个都完成,然后执行)有入参,有返回值
        CompletableFuture<Integer> cf11 = cf1.thenCombine(cf3, (r1, r2) -> {
            return r1 + r2;
        });
        System.out.println("cf11 result: " + cf11.get());
        // 12 组合处理(两个都完成,然后执行)有入参,无返回值
        CompletableFuture cf12 = cf1.thenAcceptBoth(cf3, (r1, r2) -> {
        });
        System.out.println("cf12 result: " + cf12.get());
        // 13 组合处理(两个都完成,然后执行)无入参,无返回值
        CompletableFuture cf13 = cf1.runAfterBoth(cf3, () -> {
        });
        System.out.println("cf13 result: " + cf13.get());
        // 14 组合处理(有一个完成,然后执行)有入参,有返回值
        CompletableFuture<Integer> cf14 = cf1.applyToEither(cf3, (r) -> {
            return r;
        });
        System.out.println("cf14 result: " + cf14.get());
        // 15 组合处理(有一个完成,然后执行)有入参,无返回值
        CompletableFuture cf15 = cf1.acceptEither(cf3, (r) -> {
        });
        System.out.println("cf15 result: " + cf15.get());
        // 16 组合处理(有一个完成,然后执行)无入参,无返回值
        CompletableFuture cf16 = cf1.runAfterEither(cf3, () -> {
        });
        System.out.println("cf16 result: " + cf16.get());
        // 17 方法执行后返回一个新的CompletableFuture
        CompletableFuture<Integer> cf17 = cf1.thenCompose((r) -> {
            return CompletableFuture.supplyAsync(() -> {
                return 1 + 1;
            });
        });
        System.out.println("cf17 result: " + cf17.get());
        // 18 多个任务都执行成功才会继续执行
        CompletableFuture.allOf(cf1,cf2,cf3).whenComplete((r, t) -> {
            System.out.println(r);
        });
        // 18 多个任务任意一个执行成功就会继续执行
        CompletableFuture.anyOf(cf1,cf2,cf3).whenComplete((r, t) -> {
            System.out.println(r);
        });
    }

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《Linux编程从入门到精通》是一本介绍Linux编程知识的重要参考书籍。它系统地介绍了Linux操作系统的基本原理和应用,同时深入讲解了Linux下的编程技术。该书的主要内容包括:Linux系统概述、Shell编程、C语言编程、进程和线程管理、网络编程、设备驱动编程等。 《Linux编程从入门到精通》首先介绍了Linux系统的基础知识,包括Linux发行版的选择和安装,文件系统的操作等。然后详细讲解了Shell编程,介绍了Shell脚本的基本语法和常用命令,以及如何编写简单的Shell脚本来完成自动化任务。 接着,《Linux编程从入门到精通》讲解了C语言在Linux下的应用。它介绍了C语言的基本语法和编程技巧,并通过实例演示了如何使用C语言编写Linux下的应用程序。例如,如何编写一个简单的文本编辑器或者一个网络聊天程序。 在进程和线程管理方面,《Linux编程从入门到精通》详细介绍了Linux下的进程管理和线程管理技术。它讲解了进程的创建、运行和退出过程,以及进程间通信的各种方法。同时,还介绍了线程的创建和管理方法,并讲解了线程同步和互斥的技术。 此外,《Linux编程从入门到精通》还深入讲解了网络编程和设备驱动编程等高级主题。它介绍了Linux网络编程的基本原理和常用技术,如套接字编程和网络通信协议。在设备驱动编程方面,该书介绍了Linux设备驱动的框架和开发过程,以及如何编写一个简单的字符设备驱动程序。 总之,《Linux编程从入门到精通》是一本非常实用的Linux编程指南。通过学习这本书,读者可以系统地了解Linux编程的基本知识和技巧,并能够熟练地编写各种类型的Linux程序。无论是初学者还是有一定经验的开发人员,都可以从这本书中受益匪浅。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值