Java异步回调方法

泡茶案例:为了异步执行整个泡茶流程,分别设计三个线程:泡茶线程烧水线程清洗线程

1. Join:异步阻塞

1.1 线程的合并流程

join操作的原理是阻塞当前的线程,直到待合并的目标线程执行完成

Java中线程的合并流程是:假设线程A调用线程B的join()方法去合并B线程,那么线程A进入阻塞状态,直到线程B执行完成。

1.2 调用join()实现异步泡茶喝

调用join()实现泡茶喝是一个异步阻塞版本,具体的代码实现如下:

package com.crazymakercircle.coccurent;
...
public class JoinDemo {
    public static final int SLEEP_GAP = 500;
    public static String getCurThreadName() {
        return Thread.currentThread().getName();
    }
    static class HotWarterThread extends Thread {
        public HotWarterThread() {
            super("** 烧水-Thread");
        }
        public void run() {
            try {
                Logger.info("洗好水壶");
                Logger.info("灌上凉水");
                Logger.info("放在火上");
                //线程睡眠一段时间,代表烧水中
                Thread.sleep(SLEEP_GAP);
                Logger.info("水开了");
            } catch (InterruptedException e) {
                Logger.info(" 发生异常被中断.");
            }
            Logger.info(" 运行结束.");
        }
    }
    static class WashThread extends Thread {
        public WashThread() {
            super("$$ 清洗-Thread");
        }
        public void run() {
            try {
                Logger.info("洗茶壶");
                Logger.info("洗茶杯");
                Logger.info("拿茶叶");
                //线程睡眠一段时间,代表清洗中
                Thread.sleep(SLEEP_GAP);
                Logger.info("洗完了");
            } catch (InterruptedException e) {
                Logger.info(" 发生异常被中断.");
            }
            Logger.info(" 运行结束.");
        }
    }
    public static void main(String args[]) {
        Thread hThread = new HotWarterThread();
        Thread wThread = new WashThread();
        hThread.start();
        wThread.start();

        //...在等待烧水和清洗时,可以干点其他事情

        try {
            // 合并烧水-线程
            hThread.join();
            // 合并清洗-线程
            wThread.join();
            Thread.currentThread().setName("主线程");
            Logger.info("泡茶喝");

        } catch (InterruptedException e) {
            Logger.info(getCurThreadName() + "发生异常被中断.");
        }
        Logger.info(getCurThreadName() + " 运行结束.");
    }
}

1.3 join方法详解

(1)void join():A线程等待B线程执行结束后,A线程重启执行。

(2)void join(long millis):A线程等待B线程执行一段时间,最长等待时间为millis毫秒。超过millis毫秒后,不论B线程是否结束,A线程重启执行。

(3)void join(long millis,int nanos):等待乙方线程执行一段时间,最长等待时间为millis毫秒加nanos纳秒。超过时间后,不论乙方是否结束,甲方线程都重启执行。

注意

(1)join()是实例方法不是静态方法,需要使用线程对象去调用,如thread.join()。

(2)调用join()时,不是thread所指向的目标线程阻塞,而是当前线程阻塞。

(3)只有等到thread所指向的线程执行完成或者超时,当前线程才能启动执行。

Join不能拿到合并线程的返回值,也就是被合并的线程没有返回值

join源码:

        public final synchronized void join(long millis)
         throws InterruptedException {
             long base = System.currentTimeMillis();
             long now = 0;
     
             if (millis < 0) {
                 throw new IllegalArgumentException("timeout value is negative");
             }
     
             if (millis == 0) {
                 while (isAlive()) {  
                     wait(0);  //阻塞当前线程
                 }
             } else {
                 while (isAlive()) {
                     long delay = millis - now;
                     if (delay <= 0) {
                         break;
                     }
                     wait(delay);   //限时阻塞当前线程
                     now = System.currentTimeMillis() - base;
                 }
             }
         }

实现原理是不停地检查join线程是否存活,如果join线程存活,wait(0)就永远等下去,直至join线程终止后,线程的this.notifyAll()方法会被调用(该方法是在JVM中实现的,JDK中并不会看到源码)

join()方法将退出循环,恢复业务逻辑执行。很显然这种循环检查的方式比较低效。

除此之外,调用join()缺少很多灵活性,比如实际项目中很少自己单独创建线程,而是使用Executor,这进一步减少了join()的使用场景,所以join()的使用多数停留在Demo演示上

2. FutureTask

由于Runnable有一个重要的问题,它的run()方法是没有返回值的,因此Runnable不能用在需要有返回值的场景。为了解决Runnable接口的问题,Java定义了一个新的和Runnable类似的接口——Callable接口,并且将其中被异步执行的业务处理抽象方法——run()方法改名为call()方法,但是call()方法有返回值。

2.1 通过FutureTask获取异步执行结果的步骤

(1)创建一个Callable接口的实现类,并实现它的call()方法,编写好异步执行的具体逻辑,并且可以有返回值。(2)使用Callable实现类的实例构造一个FutureTask实例。

(3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。

(4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。

(5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。

2.2 使用FutureTask实现异步泡茶喝

使用FutureTask实现异步泡茶喝,main线程可以获取烧水线程、清洗线程的执行结果,然后根据结果判断是否具备泡茶条件,如果具备泡茶条件再泡茶。

使用FutureTask实现异步泡茶喝的执行流程具体如图9-2所示。

使用FutureTask类和Callable接口进行泡茶喝的实战,代码如下:

     package com.crazymakercircle.coccurent;
     ...
     public class JavaFutureDemo {
         public static final int SLEEP_GAP = 500;
         public static String getCurThreadName() {
             return Thread.currentThread().getName();
         } 
         static class HotWarterJob implements Callable<Boolean> //①
         {
             @Override
             public Boolean call() throws Exception //②
             {
                 try {
                     Logger.info("洗好水壶");
                     Logger.info("灌上凉水");
                     Logger.info("放在火上");
                     //线程睡眠一段时间,代表烧水中
                     Thread.sleep(SLEEP_GAP);
                     Logger.info("水开了");
                 } catch (InterruptedException e) {
                     Logger.info(" 发生异常被中断.");
                     return false;
                 }
                 Logger.info(" 运行结束.");
                 return true;
             }
         }
         static class WashJob implements Callable<Boolean> {
             @Override
             public Boolean call() throws Exception {
                 try {
                     Logger.info("洗茶壶");
                     Logger.info("洗茶杯");
                     Logger.info("拿茶叶");
                     //线程睡眠一段时间,代表清洗中
                     Thread.sleep(SLEEP_GAP);
                     Logger.info("洗完了");
                 } catch (InterruptedException e) {
                     Logger.info(" 清洗工作 发生异常被中断.");
                     return false;
                 }
                 Logger.info(" 清洗工作  运行结束.");
                 return true;
             }
         }
         public static void drinkTea(boolean warterOk, boolean cupOk) {
             if (warterOk && cupOk) {
                 Logger.info("泡茶喝");
             } else if (!warterOk) {
                 Logger.info("烧水失败,没有茶喝了");
             } else if (!cupOk) {
                 Logger.info("杯子洗不了,没有茶喝了");
             }
         }
         public static void main(String args[]) {
             Thread.currentThread().setName("主线程");
            Callable<Boolean> hJob = new HotWarterJob();//③
             FutureTask<Boolean> hTask =
                     new FutureTask<>(hJob);//④
             Thread hThread = new Thread(hTask, "** 烧水-Thread");//⑤
     
             Callable<Boolean> wJob = new WashJob();//③
             FutureTask<Boolean> wTask =
                     new FutureTask<>(wJob);//④
             Thread wThread = new Thread(wTask, "$$ 清洗-Thread");//⑤
             hThread.start();
             wThread.start();

            //...在等待烧水和清洗时可以干点其他事情
     
             try {
                 boolean  warterOk = hTask.get();
                 boolean  cupOk = wTask.get();
                 drinkTea(warterOk, cupOk);
             } catch (InterruptedException e) {
                 Logger.info(getCurThreadName() + "发生异常被中断.");
             } catch (ExecutionException e) {
                 e.printStackTrace();
             }
             Logger.info(getCurThreadName() + " 运行结束.");
         }
     }

首先,在上面的泡茶喝实例代码中使用了Callable接口来替代Runnable接口,并且在call方法中返回了异步线程的执行结果。

     static class WashJob implements Callable<Boolean>
     {
         @Override
         public Boolean call() throws Exception
         {
         //业务代码,并且有执行结果返回
         }
     }

其次,从Callable异步逻辑到异步线程需要创建一个FutureTask实例,并通过FutureTask实例创建新的线程:

     Callable<Boolean> hJob = new HotWarterJob();//异步逻辑
     FutureTask<Boolean> hTask =
             new FutureTask<Boolean>(hJob); //包装异步逻辑的异步任务实例
     Thread hThread = new Thread(hTask, "** 烧水-Thread");//异步线程

最后,通过FutureTask实例取得异步线程的执行结果。一般来说,通过FutureTask实例的get方法可以获取线程的执行结果。

因为通过FutureTask的get()方法获取异步结果时,主线程也会被阻塞。这一点FutureTask和join是一致的,它们都是异步阻塞模式。

异步阻塞的效率往往比较低,被阻塞的主线程不能干任何事情,唯一能干的就是傻傻等待。原生Java API除了阻塞模式的获取结果外,并没有实现非阻塞的异步结果获取方法。如果需要用到获取的异步结果,得引入一些额外的框架,接下来将会介绍谷歌的Guava框架

3. 异步回调与主动调用

在前面的泡茶喝实例中,不论主线程调用join()进行闷葫芦式线程同步,还是使用Future.get()获取异步线程的执行结果,都属于主动模式的调用。

在泡茶喝的例子中,泡茶线程是调用线程,烧水(或者清洗)线程是被调用线程,调用线程和被调用线程之间是一种主动关系,而不是被动关系。泡茶线程需要主动获取烧水(或者清洗)线程的执行结果。

调用join()或Future.get()进行同步时,泡茶线程和烧水(或者清洗)线程之间的主动关系如图9-3所示。

如何将主动调用的方向进行反转呢?这就是异步回调。回调是一种反向的调用模式,也就是说,被调用方在执行完成后,会反向执行“调用方”所设置的钩子方法。

使用回调模式将泡茶线程和烧水(或者清洗)线程之间的“主动”关系进行反转,具体如图9-4所示。

实质上,在回调模式中负责执行回调方法的具体线程已经不再是调用方的线程(如示例中的泡茶线程),而是变成了异步的被调用方的线程(如烧水线程)。

Java中回调模式的标准实现类为CompletableFuture,由于该类出现的时间比较晚,因此很多著名的中间件如Guava、Netty等都提供了自己的异步回调模式API供开发者使用。开发者还可以使用RxJava响应式编程组件进行异步回调的开发。

4. Guava的异步回调模式

Guava是Google提供的Java扩展包,它提供了一种异步回调的解决方案。

Guava中与异步回调相关的源码处于com.google.common.util.concurrent包中。

包中的很多类都用于对java.util.concurrent的能力扩展和能力增强。比如,Guava的异步任务接口ListenableFuture扩展了Java的Future接口,实现了异步回调的能力。

4.1 详解FutureCallback

总体来说,Guava主要增强了Java而不是另起炉灶。为了实现异步回调方式获取异步线程的结果,Guava做了以下增强:

  • 引入了一个新的接口ListenableFuture,继承了Java的Future接口,使得Java的Future异步任务在Guava中能被监控非阻塞获取异步结果。
  • 引入了一个新的接口FutureCallback,这是一个独立的新接口。该接口的目的是在异步任务执行完成后,根据异步结果完成不同的回调处理,并且可以处理异步结果。

FutureCallback是一个新增的接口,用来填写异步任务执行完后的监听逻辑。FutureCallback拥有两个回调方法:

  • onSuccess()方法,在异步任务执行成功后被回调。调用时,异步任务的执行结果作为onSuccess方法的参数被传入。
  • onFailure()方法,在异步任务执行过程中抛出异常时被回调。调用时,异步任务所抛出的异常作为onFailure方法的参数被传入。

FutureCallback的源码如下:

     package com.google.common.util.concurrent;
     
     public interface FutureCallback<V> {
         void onSuccess(@Nullable V var1);
         void onFailure(Throwable var1);
     }

注意,Guava的FutureCallback与Java的Callable名字相近,实质不同,存在本质的区别:

(1)Java的Callable接口代表的是异步执行的逻辑。

(2)Guava的FutureCallback接口代表的是Callable异步逻辑执行完成之后,根据成功或者异常两种情形所需要执行的善后工作。

Guava是对Java Future异步回调的增强,使用Guava异步回调也需要用到Java的Callable接口。简单地说,只有在Java的Callable任务执行结果出来后,才可能执行Guava中的FutureCallback结果回调。

Guava如何实现异步任务Callable和结果回调FutureCallback之间的监控关系呢?Guava引入了一个新接口ListenableFuture,它继承了Java的Future接口,增强了被监控的能力。

5. CompletableFuture异步回调

很多语言(如JavaScript)提供了异步回调,一些Java中间件(如Netty、Guava)也提供了异步回调API,为开发者带来了更好的异步编程工具。

Java 8提供了一个新的、具备异步回调能力的工具类——CompletableFuture,该类实现了Future接口,还具备函数式编程的能力。

5.1 CompletableFuture详解

CompletableFuture是JDK 1.8引入的实现类,该类实现了FutureCompletionStage两个接口。

该类的实例作为一个异步任务,可以在自己异步执行完成之后触发一些其他的异步任务,从而达到异步回调的效果。

5.1.1 CompletableFuture的UML类关系

Future接口大家已经非常熟悉了,接下来介绍一下CompletionStage接口

CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会进入另一个阶段。

一个阶段可以理解为一个子任务,每一个子任务会包装一个Java函数式接口实例,表示该子任务所要执行的操作。

5.1.2 CompletionStage接口

顾名思义,Stage是阶段的意思。CompletionStage代表某个同步或者异步计算的一个阶段,或者一系列异步任务中的一个子任务(或者阶段性任务)。

每个CompletionStage子任务所包装的可以是一个FunctionConsumer或者Runnable函数式接口实例。这三个常用的函数式接口的特点如下:

(1)FunctionFunction接口的特点是:有输入、有输出。包装了Function实例的CompletionStage子任务需要一个输入参数,并会产生一个输出结果到下一步。

(2)RunnableRunnable接口的特点是:无输入、无输出。包装了Runnable实例的CompletionStage子任务既不需要任何输入参数,又不会产生任何输出。

(3)ConsumerConsumer接口的特点是:有输入、无输出。包装了Consumer实例的CompletionStage子任务需要一个输入参数,但不会产生任何输出。

多个CompletionStage构成了一条任务流水线,一个环节执行完成了可以将结果移交给下一个环节(子任务)。多个CompletionStage子任务之间可以使用链式调用,下面是一个简单的例子:

     oneStage.thenApply(x -> square(x)) 
                             .thenAccept(y -> System.out.println(y))
                             .thenRun(() -> System.out.println())

对以上例子中的CompletionStage子任务说明如下:

(1)oneStage是一个CompletionStage子任务,这是一个前提。

(2)“x -> square(x)”是一个Function类型的Lambda表达式,被thenApply()方法包装成了一个CompletionStage子任务,该子任务需要接收一个参数x,然后输出一个结果——x的平方值。

(3)“y -> System.out.println(y)”是一个Consumer类型的Lambda表达式,被thenAccept()方法包装成了一个CompletionStage子任务,该子任务需要消耗上一个子任务的输出值,但是此子任务并没有输出。

(4)“() -> System.out.println()”是一个Runnable类型的Lambda表达式,被thenRun()方法包装成了一个CompletionStage子任务,既不消耗上一个子任务的输出,又不产生结果。

CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。虽然一个子任务可以触发其他子任务,但是并不能保证后续子任务的执行顺序。

5.1.3 使用runAsync和supplyAsync创建子任务

CompletionStage子任务的创建是通过CompletableFuture完成的。

CompletableFuture类提供了非常强大的Future的扩展功能来帮助我们简化异步编程的复杂性,提供了函数式编程的能力来帮助我们通过回调的方式处理计算结果,也提供了转换组合CompletionStage()的方法

CompletableFuture定义了一组方法用于创建CompletionStage子任务(或者阶段性任务),基础的方法如下:

     //子任务包装一个Runnable实例,并调用ForkJoinPool.commonPool()线程池来执行
     public static CompletableFuture<Void> runAsync(Runnable runnable)
     
     //子任务包装一个Runnable实例,并调用指定的executor线程池来执行
     public static CompletableFuture<Void> runAsync(
                                         Runnable runnable, Executor executor)
       
     //子任务包装一个Supplier实例,并调用ForkJoinPool.commonPool()线程池来执行
     public static <U> CompletableFuture<U> supplyAsync(
                                        Supplier<U> supplier)
       
     //子任务包装一个Supplier实例,并使用指定的executor线程池来执行
     public static <U> CompletableFuture<U> supplyAsync(
                                         Supplier<U> supplier, Executor executor)

在使用CompletableFuture创建CompletionStage子任务时,如果没有指定Executor线程池,在默认情况下CompletionStage会使用公共的ForkJoinPool线程池

下面是两个创建CompletionStage子任务简单示例:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
         //创建一个无消耗值(无输入值)、无返回值的异步子任务
        @Test
        public static void runAsyncDemo() throws Exception
         {
             CompletableFuture<Void> future = CompletableFuture.runAsync(() ->
             {
                 sleepSeconds(1);//模拟执行1秒
                 Print.tcfo("run end ...");
             });
     
             //等待异步任务执行完成,限时等待2秒
             future.get(2, TimeUnit.SECONDS);
         }
     
          //创建一个无消耗值(无输入值)、有返回值的异步子任务
        @Test
         public static void supplyAsyncDemo() throws Exception
         {
             CompletableFuture<Long> future = CompletableFuture.supplyAsync(() ->
             {
                 long start = System.currentTimeMillis();
                 sleepSeconds(1);//模拟执行1秒
                 Print.tcfo("run end ...");
                 return System.currentTimeMillis() - start;
             });
     
             //等待异步任务执行完成,限时等待2秒
             long time = future.get(2, TimeUnit.SECONDS);
             Print.tcfo("异步执行耗时(秒) = " + time / 1000);
         }
         // 省略其他代码
     }

5.1.4 设置子任务回调钩子

可以为CompletionStage子任务设置特定的回调钩子,当计算结果完成或者抛出异常的时候,执行这些特定的回调钩子。

     //设置子任务完成时的回调钩子
     public CompletableFuture<T> whenComplete(
             BiConsumer<? super T,? super Throwable> action)
     
     //设置子任务完成时的回调钩子,可能不在同一线程执行
     public CompletableFuture<T> whenCompleteAsync(
             BiConsumer<? super T,? super Throwable> action)
     
     //设置子任务完成时的回调钩子,提交给线程池executor执行
     public CompletableFuture<T> whenCompleteAsync(
             BiConsumer<? super T,? super Throwable> action,
             Executor executor)
     
     //设置异常处理的回调钩子
     public CompletableFuture<T> exceptionally(
                             Function<Throwable,? extends T> fn)

下面是一个为CompletionStage子任务设置完成钩子和异常钩子的简单示例:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
         @Test
         public void whenCompleteDemo() throws Exception
         {
             //创建异步任务
             CompletableFuture<Void> future = CompletableFuture.runAsync(() ->
             {
                 sleepSeconds(1);//模拟执行1秒
                 Print.tco("抛出异常!");
                 throw new RuntimeException("发生异常");
              });
     
             //设置异步任务执行完成后的回调钩子
             future.whenComplete(new BiConsumer<Void, Throwable>()
             {
                 @Override
                 public void accept(Void t, Throwable action)
                 {
                    Print.tco("执行完成!");
                 }
             });
     
             //设置异步任务发生异常后的回调钩子
             future.exceptionally(new Function<Throwable, Void>()
             {
                 @Override
                 public Void apply(Throwable t)
                 {
                     Print.tco("执行失败!" + t.getMessage());
                     return null;
                 }
             });
     
            //获取异步任务的结果
             future.get();
           }
           // 省略其他代码
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-1]:抛出异常!
     [main]:执行完成!
     [ForkJoinPool.commonPool-worker-1]:执行失败!java.lang.RuntimeException: 发生异常

调用cancel()方法取消CompletableFuture时,任务被视为异常完成,completeExceptionally()方法所设置的异常回调钩子也会被执行。

如果没有设置异常回调钩子,发生内部异常时会有两种情况发生

1)在调用get()get(long,TimeUnit)方法启动任务时,如果遇到内部异常,get()方法就会抛出ExecutionException(执行异常)。

(2)在调用join()getNow(T)启动任务时(大多数情况下都是如此),如果遇到内部异常,join()getNow(T)方法就会抛出CompletionException

5.1.5 调用handle()方法统一处理异常和结果

除了分别通过whenCompleteexceptionally设置完成钩子、异常钩子之外,还可以调用handle()方法统一处理结果和异常。

handle()方法有三个重载版本,声明如下:

     //在执行任务的同一个线程中处理异常和结果
     public <U> CompletionStage<U> handle(
             BiFunction<? super T, Throwable, ? extends U> fn);
     
     //可能不在执行任务的同一个线程中处理异常和结果
     public <U> CompletionStage<U> handleAsync(
             BiFunction<? super T, Throwable, ? extends U> fn);
     
     //在指定线程池executor中处理异常和结果
     public <U> CompletionStage<U> handleAsync(
             BiFunction<? super T, Throwable, ? extends U> fn,
             Executor executor);

handle()方法的示例代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
         @Test
         public void handleDemo() throws Exception
         {
             CompletableFuture<Void> future = CompletableFuture.runAsync(() ->
             {
                 sleepSeconds(1);//模拟执行1秒
                 Print.tco("抛出异常!");
                 throw new RuntimeException("发生异常");
                 //Print.tco("run end ...");
             });
     
             //统一处理异常和结果
             future.handle(new BiFunction<Void, Throwable, Void>()
             {
     
                 @Override
                 public Void apply(Void input, Throwable throwable)
                 {
                     if (throwable == null)
                     {
                         Print.tcfo("没有发生异常!");
     
                     } else
                     {
                         Print.tcfo("sorry,发生了异常!");
     
                     }
                     return null;
                 }
             });
     
             future.get();
         }
         //省略其他代码
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-1]:抛出异常!
     [ForkJoinPool.commonPool-worker-1|CompletableFutureDemo$3.apply]:sorry,发生了异常!

5.1.6 线程池的使用

通过静态方法runAsync()supplyAsync()创建的CompletableFuture任务会使用公共的ForkJoinPool线程池默认的线程数是CPU的核数。当然,它的线程数可以通过以下JVM参数设置:

     option:-Djava.util.concurrent.ForkJoinPool.common.parallelism

如果所有CompletableFuture共享一个线程池,那么一旦有任务执行一些很慢的IO操作,就会导致线程池中的所有线程都阻塞在IO操作上,造成线程饥饿,进而影响整个系统的性能。所以,强烈建议大家根据不同的业务类型创建不同的线程池,以避免互相干扰

作为演示,这里使用混合型任务线程池执行CompletableFuture任务,具体的代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
     
         @Test
         public void threadPoolDemo() throws Exception
         {
             //混合线程池
             ThreadPoolExecutor pool= ThreadUtil.getMixedTargetThreadPool();
             CompletableFuture<Long> future =
                              CompletableFuture.supplyAsync(() ->
             {
                 Print.tco("run begin ...");
                 long start = System.currentTimeMillis();
                 sleepSeconds(1);//模拟执行1秒
                 Print.tco("run end ...");
                 return System.currentTimeMillis() - start;
             },pool); 
     
             //等待异步任务执行完成,限时等待2秒
             long time = future.get(2, TimeUnit.SECONDS);
             Print.tco("异步执行耗时(秒) = " + time / 1000);
         }
     
     // 省略其他代码
     }

运行程序,结果如下:

     [apppool-1-mixed-1]:run begin ...
     [apppool-1-mixed-1]:run end ...
     [main]:异步执行耗时(秒) = 1

5.2 异步任务的串行执行

5.2.1 thenApply()方法

thenApply()方法有三个重载版本,声明如下:

     //后一个任务与前一个任务在同一个线程中执行
     public <U> CompletableFuture<U> thenApply(
                                 Function<? super T,? extends U> fn)
     
     //后一个任务与前一个任务不在同一个线程中执行
     public <U> CompletableFuture<U> thenApplyAsync(
                                 Function<? super T,? extends U> fn)
     
     //后一个任务在指定的executor线程池中执行 
     public <U> CompletableFuture<U> thenApplyAsync(
                                 Function<? super T,? extends U> fn, Executor executor)

thenApply的三个重载版本有一个共同的参数fn,该参数表示要串行执行的第二个异步任务,它的类型为Function。fn的类型声明涉及两个泛型参数,具体如下:

  • 泛型参数 T:上一个任务所返回结果的类型。
  • 泛型参数 U:当前任务的返回值类型。

作为示例,调用thenApply分两步计算(10+10)*2,代码如下:

      package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
        @Test
         public void thenApplyDemo() throws Exception
         {
             CompletableFuture<Long> future =
                  CompletableFuture.supplyAsync(new Supplier<Long>()
             {
                 @Override
                 public Long get()
                 {
                     long firstStep = 10L + 10L;
                     Print.tco("firstStep outcome is " + firstStep);
     
                     return firstStep;
                 }
             }).thenApplyAsync(new Function<Long, Long>()
             {
                 @Override
                 public Long apply(Long firstStepOutCome) //传入第一步的结果
                 {
                     long secondStep = firstStepOutCome * 2;
                     Print.tco("secondStep outcome is " + secondStep);
                     return secondStep;
                 }
             });
     
             long result = future.get();
             Print.tco(" outcome is " + result);
         }
         // 省略其他代码
     }

运行以上代码,结果如下:

     [ForkJoinPool.commonPool-worker-1]:firstStep outcome is 20
     [ForkJoinPool.commonPool-worker-1]:secondStep outcome is 40
     [main]: outcome is 40

thenApply系列函数的回调参数为fn,它的类型为接口Function<T,R>,该接口的代码如下:

     @FunctionalInterface
     public interface Function<T, R> {
         R apply(T t);
     }

Function<T,R>接口既能接收参数又支持返回值,

所以thenApply可以将前一个任务的结果通过Function的R apply(T t)方法传递到第二个任务,并且能输出第二个任务的执行结果。

5.2.2 thenRun()方法

thenRun()方法与thenApply()方法不同的是,不关心任务的处理结果。只要前一个任务执行完成,就开始执行后一个串行任务。

thenApply()方法也有三个重载版本,声明如下:

     //后一个任务与前一个任务在同一个线程中执行
     public CompletionStage<Void> thenRun(Runnable action);
     
     //后一个任务与前一个任务不在同一个线程中执行
     public CompletionStage<Void> thenRunAsync(Runnable action);
     
     //后一个任务在executor线程池中执行
     public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);

从方法的声明可以看出,thenRun()方法同thenApply()方法类似,不同的是前一个任务处理完成后,thenRun()并不会把计算的结果传给后一个任务,而且后一个任务也没有结果输出。

thenRun系列方法中的action参数是Runnable类型的,所以thenRun()既不能接收参数又不支持返回值。

5.2.3 thenAccept()方法

thenAccept()方法对thenRun()、thenApply()的特点进行了折中,调用此方法时后一个任务可以接收(或消费)前一个任务的处理结果,但是后一个任务没有结果输出。

thenAccept()方法有三个重载版本,声明如下:

     //后一个任务与前一个任务在同一个线程中执行
     public CompletionStage<Void> thenAccept(Consumer<? super T> action);
     
     //后一个任务与前一个任务不在同一个线程中执行
     public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
     
     //后一个任务在指定的executor线程池中执行
     public CompletionStage<Void> thenAcceptAsync(
                             Consumer<? super T> action,Executor executor);

thenAccept系列方法的回调参数为action,它的类型为Consumer<? super T>接口,该接口的代码如下:

     @FunctionalInterface
     public interface Consumer<T> {
     
         void accept(T t);
     }

Consumer<T>接口的accept()方法可以接收一个参数,但是不支持返回值,所以thenAccept()可以将前一个任务的结果及该阶段性的结果通过void accept(T t)方法传递到下一个任务。但是Consumer<T>接口的accept()方法没有返回值,所以thenAccept()方法也不能提供第二个任务的执行结果。

5.2.4 thenCompose()方法

     public <U> CompletableFuture<U> thenCompose(
                 Function<? super T, ? extends CompletionStage<U>> fn);
     
     public <U> CompletableFuture<U> thenComposeAsync(
                 Function<? super T, ? extends CompletionStage<U>> fn) ;
     
     public <U> CompletableFuture<U> thenComposeAsync(
                 Function<? super T, ? extends CompletionStage<U>> fn, 
                 Executor executor) ;

thenCompose()方法要求第二个任务的返回值是一个CompletionStage异步实例。

因此,可以调用CompletableFuture.supplyAsync()方法将第二个任务所要调用的普通异步方法包装成一个CompletionStage异步实例。作为演示,调用thenCompose分两步计算(10+10)*2,代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     {
         @Test
         public void thenComposeDemo() throws Exception
         {
             CompletableFuture<Long> future =
                          CompletableFuture.supplyAsync(new Supplier<Long>()
             {
                 @Override
                 public Long get()
                 {
                     long firstStep = 10L + 10L;
                     Print.tco("firstStep outcome is " + firstStep);
     
                     return firstStep;
                 }
             }).thenCompose(new Function<Long, CompletionStage<Long>>()
             {
                 @Override
                 public CompletionStage<Long> apply(Long firstStepOutCome)
                 {
                //重点:将第二个任务所要调用的普通异步方法包装成一个CompletionStage异步实例
                     return CompletableFuture.supplyAsync(new Supplier<Long>()
                     {
                         //两个任务所要调用的普通异步方法
                         @Override
                         public Long get()
                         {
                             long secondStep = firstStepOutCome * 2;
                             Print.tco("secondStep outcome is " + secondStep);
                             return secondStep;
                         }
                     });
                 }
     
             });
             long result = future.get();
             Print.tco(" outcome is " + result);
         }
         // 省略其他代码
     }

这段程序的执行结果与调用thenApply()分两步计算(10+10)*2的结果是一样的。

但是,thenCompose()所返回的不是第二个任务所要执行的普通异步方法Supplier<Long>.get()的直接计算结果,而是调用CompletableFuture.supplyAsync()方法将普通异步方法Supplier<Long>.get()包装成一个CompletionStage异步实例并返回。

5.2.5 4个任务串行方法的区别

thenApply()、thenRun()、thenAccept()这三个方法的不同之处主要在于其核心参数fn、action、consumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

但是,thenCompose()方法thenApply()方法有本质的不同:

(1)thenCompose()的返回值是一个新的CompletionStage实例,可以持续用来进行下一轮CompletionStage任务的调度。具体来说,thenCompose()返回的是包装了普通异步方法的CompletionStage任务实例,通过该实例还可以进行下一轮CompletionStage任务的调度和执行,比如可以持续进行CompletionStage链式(或者流式)调用。

(2)thenApply()的返回值则简单多了,直接就是第二个任务的普通异步方法的执行结果,它的返回类型与第二步执行的普通异步方法的返回类型相同,通过thenApply()所返回的值不能进行下一轮CompletionStage链式(或者流式)调用

5.3 异步任务的合并执行

对两个异步任务的合并可以通过CompletionStage接口的thenCombine()runAfterBoth()thenAcceptBoth()三个方法来实现。

这三个方法的不同之处主要在于其核心参数fnactionconsumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

5.3.1 thenCombine()方法

thenCombine()会在两个CompletionStage任务都执行完成后,把两个任务的结果一起交给thenCombine()来处理。

     //合并代表第二步任务(参数other)的CompletionStage实例,返回第三步任务的CompletionStage
     public <U,V> CompletionStage<V> thenCombine(
             CompletionStage<? extends U> other, //待合并CompletionStage实例
             BiFunction<? super T,? super U,? extends V> fn); //第三步的逻辑
     
     //不一定在同一个线程中执行第三步任务的CompletionStage实例
     public <U,V> CompletionStage<V> thenCombineAsync(
             CompletionStage<? extends U> other,
             BiFunction<? super T,? super U,? extends V> fn);
     
     //第三步任务的CompletionStage实例在指定的executor线程池中执行
     public <U,V> CompletionStage<V> thenCombineAsync(
             CompletionStage<? extends U> other,
             BiFunction<? super T,? super U,? extends V> fn,
             Executor executor);

thenCombine()方法的调用者为第一步的CompletionStage实例,该方法的第一个参数为第二步的CompletionStage实例,该方法的返回值为第三步的CompletionStage实例。

在逻辑上,thenCombine()方法的功能是将第一步、第二步的结果合并到第三步上。thenCombine系列方法有两个核心参数

  • other参数:表示待合并的第二步任务的CompletionStage实例。
  • fn参数:表示第一个任务和第二个任务执行完成后,第三步需要执行的逻辑。

fn参数的类型为BiFunction<? super T,? super U,? extends V>,该类型的声明涉及三个泛型参数,具体如下:

  • 泛型参数 T:表示第一个任务所返回结果的类型。
  • 泛型参数 U:表示第二个任务所返回结果的类型。
  • 泛型参数 V:表示第三个任务所返回结果的类型

BiFunction<? super T,? super U,? extends V>的源码如下:

     @FunctionalInterface
     public interface BiFunction<T, U, R> {
         R apply(T t, U u);
     }

通过BiFunction的apply()方法的源码可以看出,BiFunction的前两个泛型参数T、U是输入参数的类型,BiFunction的后一个泛型参数V是输出参数的类型。作为示例,接下来调用thenCombine分三步计算(10+10)*(10+10),代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
     
         @Test
         public void thenCombineDemo() throws Exception
         {
             CompletableFuture<Integer> future1 =
                     CompletableFuture.supplyAsync(new Supplier<Integer>()
                     {
                         @Override
                         public Integer get()
                         {
                             Integer firstStep = 10 + 10;
                             Print.tco("firstStep outcome is " + firstStep);
                             return firstStep;
                         }
                     });
             CompletableFuture<Integer> future2 =
                     CompletableFuture.supplyAsync(new Supplier<Integer>()
                     {
                         @Override
                         public Integer get()
                         {
                             Integer secondStep = 10 + 10;
                             Print.tco("secondStep outcome is " + secondStep);
                             return secondStep;
                         }
                     });
             CompletableFuture<Integer> future3 = future1.thenCombine(future2,
                     new BiFunction<Integer, Integer, Integer>()
                     {
                         @Override
                         public Integer apply(
                             Integer step1OutCome, Integer step2OutCome)
                         {
                             return step1OutCome * step2OutCome;
                         }
                     });
             Integer result = future3.get();
             Print.tco(" outcome is " + result);
         }
         // 省略其他代码
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-1]:firstStep outcome is 20
     [ForkJoinPool.commonPool-worker-2]:secondStep outcome is 20
     [main]: outcome is 400

5.3.2 runAfterBoth()方法

runAfterBoth()方法跟thenCombine()方法不一样的是,runAfterBoth()方法不关心每一步任务的输入参数和处理结果。

runAfterBoth()方法有三个重载版本,声明如下:

     //合并第二步任务的CompletionStage实例,返回第三步任务的CompletionStage
     public CompletionStage<Void> runAfterBoth(
                     CompletionStage<?> other,Runnable action);
     
     //不一定在同一个线程中执行第三步任务的CompletionStage实例
     public CompletionStage<Void> runAfterBothAsync(
                     CompletionStage<?> other,Runnable action);
     
     //第三步任务的CompletionStage实例在指定的executor线程池中执行
     public CompletionStage<Void> runAfterBothAsync(
                     CompletionStage<?> other,Runnable action,
                     Executor executor);

runAfterBoth()方法的调用者为第一步任务的CompletionStage实例,runAfterBoth()方法的第一个参数为第二步任务的CompletionStage实例,runAfterBoth()方法的返回值为第三步的CompletionStage实例。

在逻辑上,第一步任务和第二步任务是并行执行的,thenCombine()方法的功能是将第一步、第二步的结果合并到第三步任务上。

与thenCombine系列方法不同,runAfterBoth系列方法的第二个参数action为Runnable类型,表示它的第一步任务、第二步任务、第三步任务既没有输入值,又没有输出值。

5.3.3 thenAcceptBoth()方法

thenAcceptBoth()方法对runAfterBoth()方法和thenCombine()方法的特点进行了折中,调用该方法,第三个任务可以接收其合并过来的第一个任务、第二个任务的处理结果,但是第三个任务(合并任务)却不能返回结果。thenAcceptBoth()方法有三个重载版本,三个版本的声明如下:

     //合并第二步任务的CompletionStage实例,返回第三步任务的CompletionStage
     public <U> CompletionStage<Void> thenAcceptBoth(
                     CompletionStage<? extends U> other,
                     BiConsumer<? super T, ? super U> action);
     
     //功能与上一个方法相同,不一定在同一个线程执行第三步任务
     public <U> CompletionStage<Void> thenAcceptBothAsync(
                     CompletionStage<? extends U> other,
                     BiConsumer<? super T, ? super U> action);
     
     //功能与上一个方法相同,在指定的executor线程池中执行第三步任务
     public <U> CompletionStage<Void> thenAcceptBothAsync(
                     CompletionStage<? extends U> other,
                     BiConsumer<? super T, ? super U> action,
                     Executor executor);

thenAcceptBoth系列方法的第二个参数为需要合并的第二步任务的CompletionStage实例。

第三个参数为第三个任务的回调函数,该参数名称为action,它的类型为BiConsumer<? super T,? super U>接口,该接口的代码如下

     @FunctionalInterface
     public interface BiConsumer<T, U> {
         void accept(T t, U u);
     }

BiConsumer<? super T,? super U>接口的accept()方法可以接收两个参数,但是不支持返回值。

所以thenAcceptBoth()可以将前面的第一个任务、第二个任务的结果作为阶段性的结果进行合并。

但是BiConsumer<T,U>的accept()方法没有返回值,所以thenAccept()不能提供第三个任务的执行结果。

10.3.4 allOf()

等待所有的任务结束CompletionStage接口的allOf()会等待所有的任务结束,以合并所有的任务。thenCombine()只能合并两个任务,如果需要合并多个异步任务,那么可以调用allOf()。

一个简单的实例如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class CompletableFutureDemo
     {
     
         @Test
         public void allOfDemo() throws Exception
         {
             CompletableFuture<Void> future1 =
                     CompletableFuture.runAsync(() -> Print.tco("模拟异步任务1"));
     
             CompletableFuture<Void> future2 =
                     CompletableFuture.runAsync(() -> Print.tco("模拟异步任务2"));
             CompletableFuture<Void> future3 =
                     CompletableFuture.runAsync(() -> Print.tco("模拟异步任务3"));
             CompletableFuture<Void> future4 =
                     CompletableFuture.runAsync(() -> Print.tco("模拟异步任务4"));
     
             CompletableFuture<Void> all =
                     CompletableFuture.allOf(future1, future2, future3, future4);
             all.join();
         }
        // 省略其他代码
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-1]:模拟异步任务1
     [ForkJoinPool.commonPool-worker-4]:模拟异步任务4
     [ForkJoinPool.commonPool-worker-3]:模拟异步任务3
     [ForkJoinPool.commonPool-worker-2]:模拟异步任务2

5.4 异步任务的选择执行

CompletableFuture对异步任务的选择执行不是按照某种条件进行选择的,而是按照执行速度进行选择的:前面两个并行任务,谁的结果返回速度快,谁的结果将作为第三步任务的输入。

对两个异步任务的选择可以通过CompletionStage接口的applyToEither()、runAfterEither()和acceptEither()三个方法来实现。

这三个方法的不同之处在于它的核心参数fn、action、consumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

5.4.1 applyToEither()方法

两个CompletionStage谁返回结果的速度快,applyToEither()方法就用这个最快的CompletionStage的结果进行下一步(第三步)的回调操作。

applyToEither()方法有三个重载版本,三个版本的声明如下:

     //和other任务进行速度PK,最快返回的结果用于执行fn回调函数
     public <U> CompletionStage<U> applyToEither(
             CompletionStage<? extends T> other,Function<? super T, U> fn);
     
     //功能与上一个方法相同,不一定在同一个线程中执行fn回调函数
     public <U> CompletionStage<U> applyToEitherAsync(
             CompletionStage<? extends T> other,Function<? super T, U> fn);
     
     //功能与上一个方法相同,在指定线程执行fn回调函数
     public <U> CompletionStage<U> applyToEitherAsync(
             CompletionStage<? extends T> other,
             Function<? super T, U> fn,Executor executor);

applyToEither系列方法的回调参数为fn,它的类型为接口Function<T,R>,该接口的代码如下:

     @FunctionalInterface
     public interface Function<T, R> {
         R apply(T t);
     }

Function<T,R>接口既能接收输入参数又支持返回值。

在applyToEither()方法中,Function的输入参数为前两个CompletionStage中返回快的那个结果,

Function的输出值为最终的执行结果。作为示例,接下来调用applyToEither随机选择(10+10)和(100+100)的结果,代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     {
         @Test
         public void applyToEitherDemo() throws Exception
         {
             CompletableFuture<Integer> future1 =
                     CompletableFuture.supplyAsync(new Supplier<Integer>()
                     {
                         @Override
                         public Integer get()
                         {
                             Integer firstStep = 10 + 10;
                             Print.tco("firstStep outcome is " + firstStep);
                             return firstStep;
                         }
                     });
             CompletableFuture<Integer> future2 =
                     CompletableFuture.supplyAsync(new Supplier<Integer>()
                     {
                         @Override
                         public Integer get()
                         {
                             Integer secondStep = 100 + 100;
                             Print.tco("secondStep outcome is " + secondStep);
                             return secondStep;
                         }
                     });
     
                   //谁返回结果快,谁的结果将被第三步选择到
             CompletableFuture<Integer> future3 = 
                                     future1.applyToEither(future2,
                     new Function<Integer, Integer>()
                     {
                         @Override
                         public Integer apply(Integer eitherOutCome)
                         {
                             return eitherOutCome;
                         }
                     });
             Integer result = future3.get();
             Print.tco(" outcome is " + result);
         }
         // 省略其他代码
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-1]:firstStep outcome is 20
     [ForkJoinPool.commonPool-worker-2]:secondStep outcome is 200
     [main]: outcome is 200

以上示例中,由于commonPool-worker-1、commonPool-worker-2两个线程的调度具有随机性,因此输出结果有时是200,有时20。

5.4.2 runAfterEither()方法

runAfterEither()方法的功能为:前面两个CompletionStage实例,任何一个完成了都会执行第三步回调操作。

三个任务的回调函数都是Runnable类型的。runAfterEither()方法有三个重载版本,声明如下:

     //和other任务进行速度PK,只要一个执行完成,就开始执行fn回调函数[Jun Zhao1]
     public CompletionStage<Void> runAfterEither(
                     CompletionStage<?> other,Runnable action);
     
     //功能与上一个方法相同,不一定在同一个线程中执行fn回调函数[Jun Zhao2]
     public CompletionStage<Void> runAfterEitherAsync(
                     CompletionStage<?> other,Runnable action);
     
     //功能与上一个方法相同,在指定线程执行fn回调函数[Jun Zhao3]
     public CompletionStage<Void> runAfterEitherAsync(
                     CompletionStage<?> other,Runnable action,
                     Executor executor);

runAfterEither()方法的调用者为第一步任务的CompletionStage实例,

runAfterEither()方法的第一个参数为第二步任务的CompletionStage实例runAfterEither()方法的返回值为第三步任务的CompletionStage实例。

调用runAfterEither()方法,只要前面两个CompletionStage实例其中一个执行完成,就开始执行第三步的CompletionStage实例。

5.4.3 acceptEither()方法

acceptEither()方法对applyToEither()方法和runAfterEither()方法的特点进行了折中,两个CompletionStage谁返回结果的速度快,acceptEither()就用那个最快的CompletionStage的结果作为下一步(第三步)的输入,但是第三步没有输出。acceptEither()方法有三个重载版本,声明如下:

     //和other任务进行速度PK,最快返回的结果用于执行fn回调函数[Jun Zhao4]
     public CompletionStage<Void> acceptEither(
             CompletionStage<? extends T> other,
             Consumer<? super T> action);
     
     //功能与上一个方法相同,不一定在同一个线程中执行fn回调函数[Jun Zhao5]
     public CompletionStage<Void> acceptEitherAsync(
             CompletionStage<? extends T> other,
             Consumer<? super T> action);
     
     //功能与上一个方法相同,在指定的executor线程池中执行第三步任务[Jun Zhao6]
     public CompletionStage<Void> acceptEitherAsync(
             CompletionStage<? extends T> other,
             Consumer<? super T> action,Executor executor);

acceptEither系列方法的第一个参数other为待进行速度比较的CompletionStage实例,它的第二个参数为第三个任务的异步回调函数,该参数名称为action,它的类型为Consumer<? super T>接口,该接口的代码如下:

     @FunctionalInterface
     public interface Consumer<T> {
     
         void accept(T t);
     }

Consumer<T>接口的accept()可以接收一个参数,但是不支持返回值,所以acceptEither()可以将前面最快返回的阶段性结果通过void accept(T t)方法传递到第三个任务。但是Consumer<T>接口的accept()方法没有返回值,所以acceptEither()不能提供第三个任务的执行结果。

5.5 CompletableFuture的综合案例

本节基于CompletableFuture来实现前面介绍的泡茶喝实例和RPC异步调用实例。

5.5.1 使用CompletableFuture实现泡茶喝实例

为了领略CompletableFuture异步编程的优势,下面用CompletableFuture重新实现前面提及的泡茶喝程序。

首先需要先完成分工方案,在下面的程序中,我们分3个任务:任务1负责洗水壶、烧开水,任务2负责洗茶壶、洗茶杯和拿茶叶,任务3负责泡茶。

其中任务3要等待任务1和任务2都完成后才能开始。基于CompletableFuture框架实现的泡茶喝程序代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class DrinkTea
     {
         private static final int SLEEP_GAP = 3;//等待3秒
     
         public static void main(String[] args)
         {
             // 任务 1:洗水壶 -> 烧开水
             CompletableFuture<Boolean> hotJob =
                     CompletableFuture.supplyAsync(() ->
                     {
                         Print.tcfo("洗好水壶");
                         Print.tcfo("烧开水");
     
                         //线程睡眠一段时间,代表烧水中
                         sleepSeconds(SLEEP_GAP);
                         Print.tcfo("水开了");
                         return true;
     
                     });
     
             // 任务 2:洗茶壶 -> 洗茶杯 -> 拿茶叶
             CompletableFuture<Boolean> washJob =
                     CompletableFuture.supplyAsync(() ->
                     {
                         Print.tcfo("洗茶杯");
                         //线程睡眠一段时间,代表清洗中
                         sleepSeconds(SLEEP_GAP);
                         Print.tcfo("洗完了");
     
                         return true;
                     });
     
             // 任务 3:任务 1 和任务 2 完成后执行泡茶
             CompletableFuture<String> drinkJob=
                     hotJob.thenCombine(washJob, (hotOk, washOK) ->
                     {
                         if (hotOk && washOK)
                         {
                             Print.tcfo("泡茶喝,茶喝完");
                             return "茶喝完了";
                         }
                         return "没有喝到茶";
                     });
     
             // 等待任务 3 执行结果
             Print.tco(drinkJob.join());
         }
     }

执行程序,结果如下:

     [ForkJoinPool.commonPool-worker-2|DrinkTea.lambda$main$1]:洗茶杯
     [ForkJoinPool.commonPool-worker-1|DrinkTea.lambda$main$0]:洗好水壶
     [ForkJoinPool.commonPool-worker-1|DrinkTea.lambda$main$0]:烧开水
     [ForkJoinPool.commonPool-worker-2|DrinkTea.lambda$main$1]:洗完了
     [ForkJoinPool.commonPool-worker-1|DrinkTea.lambda$main$0]:水开了
     [ForkJoinPool.commonPool-worker-2|DrinkTea.lambda$main$2]:泡茶喝,茶喝完
     [main]:茶喝完了

以上结果,烧水线程为commonPool-worker-1,清洗线程为commonPool-worker-2。通过整体的执行过程可以发现:

(1)给任务分配线程的工作由框架自动完成,没有烦琐的手工维护线程的工作,当然也无须手工维护线程。(2)任务之间的依赖关系一目了然。以下面的伪代码为例:

     job3 = job1.thenCombine( job2, (result1,result2)->{回调逻辑})

以上伪代码能够清晰地表述“任务3要等待任务1和任务2都完成后才能开始”。所以,使用CompletableFuture框架能使得代码更加简练、并发逻辑更加清晰。

5.5.2 使用CompletableFuture进行多个RPC调用

使用CompletableFuture进行多个RPC调用,参考代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class IntegrityDemo
     {
         /**
          * 模拟RPC调用1
          */
         public String rpc1()
         {
             //睡眠400毫秒,模拟执行耗时
             sleepMilliSeconds(600);
             Print.tcfo("模拟RPC调用:服务器 server 1");
             return "sth. from server 1";
         }
     
         /**
          * 模拟RPC调用2
          */
         public String rpc2()
         {
             //睡眠400毫秒,模拟执行耗时
             sleepMilliSeconds(600);
             Print.tcfo("模拟RPC调用:服务器 server 2");
             return "sth. from server 2";
         }
     
         @Test
         public void rpcDemo() throws Exception
         {
             CompletableFuture<String> future1 = 
                             CompletableFuture.supplyAsync(() ->
             {
                 return rpc1();
             });
     
             CompletableFuture<String> future2 = 
                             CompletableFuture.supplyAsync(() -> rpc2());
            
             CompletableFuture<String> future3 =
                              future1.thenCombine(future2,
                     (out1, out2) ->
                     {
                         return out1 + " & " + out2;
                     });
     
             String result = future3.get();
             Print.tco("客户端合并最终的结果:" + result);
         }
     }

运行程序,结果如下:

     [ForkJoinPool.commonPool-worker-2|IntegrityDemo.rpc2]:模拟RPC调用:服务器 server 2
     [ForkJoinPool.commonPool-worker-1|IntegrityDemo.rpc1]:模拟RPC调用:服务器 server 1
     [main]:客户端合并最终的结果:sth. from server 1 & sth. from server 2

5.5.3 使用RxJava模拟RPC异步回调

除了使用CompletableFuture组件外,很多Android程序员会使用RxJava实现RPC异步回调,类似的代码如下:

     package com.crazymakercircle.completableFutureDemo;
     // 省略import
     public class IntegrityDemo
     {
        // 省略重复代码
         @Test
         public void rxJavaDemo() throws Exception
         {
             Observable<String> observable1 = 
                             Observable.fromCallable(() ->
             {
                 return rpc1();
             }).subscribeOn(Schedulers.newThread());
           
           Observable<String> observable2 =
                             Observable.fromCallable(() -> rpc2())
                             .subscribeOn(Schedulers.newThread());
     
             Observable.merge(observable1, observable2)
                     .observeOn(Schedulers.newThread())
                     .toList()
                     .subscribe(
                     (result) -> Print.tco("客户端合并最终的结果:" + result));
     
             sleepSeconds(Integer.MAX_VALUE);
         }
     
     }

运行程序,结果如下:

     [RxNewThreadScheduler-2|IntegrityDemo.rpc2]:模拟RPC调用:服务器 server 2
     [RxNewThreadScheduler-1|IntegrityDemo.rpc1]:模拟RPC调用:服务器 server 1
     [RxNewThreadScheduler-3]:客户端合并最终的结果:[sth. from server 1, sth. from server 2]

来源:《Java高并发核心编程.卷2,多线程、锁、JMM、JUC》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值