Java异步编程CompletableFuture

186 阅读9分钟

Future

从下图中可以看出CompletableFuture实现的是Future接口,我们首先回顾一下Future image.png Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行多个任务,其中一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务,处理完成后,再通过Future获取计算结果。

通过代码就行展示:一个是查询商品信息,一个计算商品的价格。

public class FutureDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        String commodityId = "1";
        long startTime = System.currentTimeMillis();

        //获取商品信息(返回商品名)
        FutureTask<String> commodityInfoTask = new FutureTask<>(() -> getCommodity(commodityId));
        executorService.submit(commodityInfoTask);
        //模拟主线程其它操作耗时
        Thread.sleep(300);
        //计算商品的价格
        FutureTask<Integer> calculatePriceTask = new FutureTask<>(() -> calculatePrice(commodityId));
        executorService.submit(calculatePriceTask);

        String commodityName = commodityInfoTask.get();
        Integer price = calculatePriceTask.get();
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");

    }

    //获取商品信息(返回商品名)
    public static String getCommodity(String commodityId) throws InterruptedException {
        //模拟查询数据库耗时
        Thread.sleep(500);
        return String.valueOf(RandomUtils.nextInt(1, 10));
    }

    //计算商品的价格
    public static Integer calculatePrice(String commodityId) throws InterruptedException {
        //模拟查询数据库耗时
        Thread.sleep(1000);
        return RandomUtils.nextInt(1, 10);
    }

}

结果显示

image.png 如果是主线程串行执行的话,总的时间应该是500+1000+300=1800ms,可以发现,future+线程池 异步配合,提高了程序的执行效率。 但是Future对于结果的获取,不是很友好,只能通过阻塞 或者轮询的方式 得到任务的结果。

  • Future.get() 就是阻塞调用,在线程获取结果之前get方法会一直阻塞 。
  • Future提供了一个isDone方法,可以在程序中轮询这个方法查询 执行结果。

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源 。因此,JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

CompletableFuture

使用CompletableFuture改造上Future的例子

public class CompletableFutureDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        String commodityId = "1";
        long startTime = System.currentTimeMillis();

        //获取商品信息(返回商品名)
        CompletableFuture<String> commodityInfoTask = CompletableFuture.supplyAsync(() -> getCommodity(commodityId));

        //模拟主线程其它操作耗时
        Thread.sleep(300);
        //计算商品的价格
        CompletableFuture<Integer> calculatePriceTask = CompletableFuture.supplyAsync(() -> calculatePrice(commodityId));

        String commodityName = commodityInfoTask.get();
        Integer price = calculatePriceTask.get();
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
    }
}

可以发现,使用CompletableFuture,代码简洁了很多。CompletableFuture的supplyAsync方法,提供了异步执行的功能,线程池也不用单独创建了。实际上,它CompletableFuture使用了默认线程池是ForkJoinPool.commonPool 。

image.png

![image.png](p3-juejin.byteimg.com/tos-cn-i-k3…? CompletableFuture提供了几十种方法,辅助我们的异步任务场景。这些方法包括创建异步任务、任务异步回调、多个任务组合处理 等方面

CompletableFuture创建异步任务

CompletableFuture创建异步任务,有supplyAsync和runAsync两个方法

  • supplyAsync执行CompletableFuture任务,支持返回值
  • runAsync执行CompletableFuture任务,没有返回值。

supplyAsync方法

一个采用默认的线程,一个自定义线程池,有返回值的异步任务 image.png

runAsync

一个采用默认的线程,一个自定义线程池,无返回值的异步任务 image.png

任务异步回调

thenRun/thenRunAsync

thenRun/thenRunAsync,做完第一个任务后,再做第二个任务 。某个任务执行完成后,执行回调方法;但是前后两个任务没有参数传递,第二个任务也没有返回值

如果你执行第一个任务的时候,传入了一个自定义线程池:

  • 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池 。
  • 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
  • 调用thenRunAsync执行第二个任务时,同时可以传入自定义的线程池 image.png 代码示例:
public class CompletableFutureThenRunTest {
    public static void main(String[] args) {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("firstTask");
            return "firstTask";
        });
        
        firstTask.thenRunAsync(()->{
            System.out.println("thenRunTask");
        });
    }
}
运行结果:
firstTask
thenRunTask

thenAccept/thenAcceptAsync

thenAccept/thenAcceptAsync方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,但是回调方法是没有返回值的。thenAccept/thenAcceptAsync的区别跟上述的thenRun/thenRunAsync一致 image.png 代码示例:

public class CompletableFutureThenAcceptTest {
    public static void main(String[] args) {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("firstTask");
            return "firstTask";
        });
        firstTask.thenAccept((firstTaskReturn) -> {
            System.out.println("finish " + firstTaskReturn);
        });
    }
}

运行结果:
firstTask
finish firstTask

thenApply/thenApplyAsync

thenApply/thenApplyAsync方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,并且回调方法是有返回值的。 thenApply/thenApplyAsync的区别跟上述的thenRun/thenRunAsync一致 image.png 代码示例:

public class CompletableFutureThenApplyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("firstTask");
            return "firstTask";
        });
        CompletableFuture<String> thenApplyTask = firstTask.thenApply((firstTaskReturn) -> {
            System.out.println("finish " + firstTaskReturn);
            return "thenApplyTask";
        });
        System.out.println(thenApplyTask.get());
    }
}
运行结果:
firstTask
finish firstTask
thenApplyTask

exceptionally

exceptionally方法表示,某个任务执行异常时,执行的回调方法;并且有抛出异常作为参数 ,传递到回调方法。 image.png 代码示例:

public class CompletableFutureExceptionallyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("currentThread:" + Thread.currentThread().getName());
            throw new RuntimeException();
        });
        CompletableFuture<String> exceptionally = firstTask.exceptionally((exception) -> {
            exception.printStackTrace();
            return "taskException";
        });
        System.out.println(exceptionally.get());
    }
}
运行结果:
currentThread:ForkJoinPool.commonPool-worker-1
java.util.concurrent.CompletionException: java.lang.RuntimeException
	at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
	at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
	at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1592)
	at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException
	at com.xhh.study.future.CompletableFutureExceptionallyTest.lambda$main$0(CompletableFutureExceptionallyTest.java:10)
	at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
	... 5 more
taskException

whenComplete/whenCompleteAsync

whenComplete/whenCompleteAsync方法表示,某个任务执行完成后,执行的回调方法,无返回值 ;并且whenComplete方法返回的CompletableFuture的result是上个任务的结果 。 whenComplete/whenCompleteAsync的区别跟上述的thenRun/thenRunAsync一致 image.png 代码示例:

public class CompletableFutureWhenCompleteTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("firstTask");
            return "firstTask";
        });
        CompletableFuture<String> whenCompleteTask = firstTask.whenComplete((firstTaskReturn, throwable) -> {
            System.out.println("firstTaskReturn:" + firstTaskReturn);
        });

        System.out.println(whenCompleteTask.get());
    }
}
结束展示:
firstTask
firstTaskReturn:firstTask
firstTask

handle/handleAsync

handle/handleAsync方法表示,某个任务执行完成后,执行回调方法,并且是有返回值的 ;并且handle方法返回的CompletableFuture的result是回调方法执行的结果。

handle/handleAsync的区别跟上述的thenRun/thenRunAsync一致

image.png 代码示例:

public class CompletableFutureHandleTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("firstTask");
            return "firstTask";
        });
        CompletableFuture<String> handle = firstTask.handle((firstTaskReturn, throwable) -> {
            System.out.println("firstTaskReturn:" + firstTaskReturn);
            return "handleResult";
        });
        System.out.println(handle.get());
    }

}
结果展示:
firstTask
firstTaskReturn:firstTask
handleResult

whenComplete和handle的区别除了handle的回调方法有返回值之外,whenComplete流程是如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致,如果该任务正常执行,则get方法返回执行结果,如果是执行异常,则get方法抛出异常。handle流程返回的CompletableFuture的result是回调方法的执行结果或者回调方法执行期间抛出的异常,与原始CompletableFuture的result无关了

多个任务组合处理

And组合关系

thenCombine / thenAcceptBoth / runAfterBoth都表示:将两个CompletableFuture组合起来,只有这两个都正常执行完了,才会执行某个任务 。

区别在于:

  • thenCombine:会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值
  • thenAcceptBoth: 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
  • runAfterBoth 不会把执行结果当做方法入参 ,且没有返回值。 代码示例:
public class CompletableFutureThenCombine {
    public static void main(String[] args) {
        CompletableFuture<String> firstTask = CompletableFuture.completedFuture("firstTask");
        //first->第一个任务的result,second第二个任务的result
        CompletableFuture<String> combineTask = CompletableFuture.supplyAsync(() -> "secondTask").thenCombine(firstTask, (second, first) -> {
            System.out.println(first);
            System.out.println(second);
            return "taskCombine";
        });
        System.out.println(combineTask.join());
    }
}
运行结果:
firstTask
secondTask
taskCombine

OR组合关系

applyToEither/acceptEither/runAfterEither 都表示:将两个CompletableFuture组合起来,只要其中一个执行完了,就会执行某个任务

区别在于:

  • applyToEither:会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值
  • acceptEither: 会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值
  • runAfterEither:不会把执行结果当做方法入参,且没有返回值。 代码示例:
public class CompletableFutureAcceptEither {
    public static void main(String[] args) {
        CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(() -> {
            //第一个异步任务,休眠2秒,保证它执行晚点
            try {
                Thread.sleep(3000);
                System.out.println("start firstTask");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "firstTask";
        });
        CompletableFuture.supplyAsync(() -> {
            System.out.println("start secondTask");
            return "secondTask";
        }).acceptEither(firstTask, System.out::println);
    }
}
结果展示:
start secondTask
secondTask

ALLOF

所有任务都执行完成后,才执行allOf返回的CompletableFuture。如果任意一个任务异常,allOf的CompletableFuture,执行get方法会抛出异常 代码示例:

public class CompletableFutureAllOf {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> firstTask = CompletableFuture.runAsync(() -> {
            System.out.println("start firstTask");
        });
        CompletableFuture<Void> secondTask = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("start secondTask");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        CompletableFuture<Void> thirdTask = CompletableFuture.runAsync(() -> {
            System.out.println("start thirdTask");
        });
        CompletableFuture<Void> allOfTask = CompletableFuture.allOf(firstTask, secondTask, thirdTask);
        allOfTask.get();
    }
}
结果展示:
start firstTask
start thirdTask
start secondTask

AnyOf

任意一个任务执行完,就执行anyOf返回的CompletableFuture。如果执行的任务异常,anyOf的CompletableFuture,执行get方法,会抛出异常

代码示例:

public class CompletableFutureAnyOf {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> firstTask = CompletableFuture.runAsync(() -> {
            System.out.println("start firstTask");
        });
        CompletableFuture<Void> secondTask = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("start secondTask");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        CompletableFuture<Void> thirdTask = CompletableFuture.runAsync(() -> {
            System.out.println("start thirdTask");
        });
        CompletableFuture<Object> anyOfTask = CompletableFuture.anyOf(firstTask, secondTask, thirdTask)
                .whenComplete((result,throwable)->{
                    System.out.println("finish anyOf Task");
                });

        anyOfTask.get();
    }
}
结果展示:
start firstTask
start thirdTask
finish anyOf Task

thenCompose

thenCompose方法会在某个任务执行完成后,将该任务的执行结果,作为方法入参,去执行指定的方法。该方法会返回一个新的CompletableFuture实例

  • 如果该CompletableFuture实例的result不为null,则返回一个基于该result新的CompletableFuture实例;
  • 如果该CompletableFuture实例为null,然后就执行这个新任务 代码示例:
public class CompletableFutureThenCompose {

    public static void main(String[] args) {
        CompletableFuture<String> firstTask = CompletableFuture.completedFuture("firstTask");
        CompletableFuture<String> composeTask = CompletableFuture.supplyAsync(() -> {
            System.out.println("start secondTask");
            return "secondTask";
        }).thenCompose(secondTaskReturn -> {
            System.out.println(secondTaskReturn);
            return firstTask;
        });
    System.out.println(composeTask.join());
    }
}
结果展示:
start secondTask
secondTask
firstTask

使用时的注意点

  1. 异步任务中可能会存在异常信息,同时需要获取到返回值才能获取到异常信息,如果不加 get()/join()方法,看不到异常信息。考虑使用异常捕获,或者exceptionally和whenComplete进行处理
  2. get和join方法是阻塞的,get方法建议加上阻塞时间。同时join()方法抛出的是uncheck异常(即未经检查的异常),不会强制开发者抛出,会将异常包装成CompletionException异常 /CancellationException异常,但是本质原因还是代码内存在的真正的异常,get()方法抛出的是经过检查的异常,ExecutionException, InterruptedException 需要用户手动处理(抛出或者 try catch)
  3. 默认的线程池处理的线程个数是电脑CPU核数-1。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢 。一般建议使用自定义线程池,优化线程池配置参数。同时注意线程池拒绝策略是DiscardPolicy或者DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛弃异常。因此建议,CompletableFuture线程池策略最好使用AbortPolicy ,然后耗时的异步线程,做好线程池隔离