异步编程CompletableFuture中异步任务创建

379 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看: 8月更文挑战

CompletableFuture是JDK8提供的一个类,该类实现了接口:Future和CompletionStage这两个接口,在原有的Future实现类上提供了比较多的API扩展功能,可以通过Stream形式简化编程的复杂度,同时也可以通过回调方式处理结算结果,也可以等到一个任务执行完成后触发后面的流程并且可以将第一个任务的执行结果带入到第二个任务中。 下面将从源码及应用层来慢慢剥开它的神秘面纱,体会大家都说好。

CompletableFuture中,提供了四种创建异步任务的静态方法

CompletableFuture<Void> runAsync(Runnable runnable);

CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor); 
    
<U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
    
<U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor);

从这方法名中可以看出,runAsync有两个重载方法,只是第二个函数第二个参数需要传入子自定义的线程池,具体这两个方法,我们跟随源码及实际调用来一探究竟,supplyAsync这个与runAsync比较相试,只是这个方法会有返回值返回,其他都和runAsync类似的。

runAsync 解析与应用

  • CompletableFuture runAsync(Runnable runnable)
  public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }
  • CompletableFuture runAsync(Runnable runnable,  Executor executor)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
}

从上面的源码可以看出其内部都会调用asyncRunStage来执行,只是传入的线程池不一样,接下来我们一步步来分析两个函数传入的线程池是咋回事?先看第一个系统自己默认的线程池

 private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

源码看出线程池会根据useCommonPool这个值来选择对应的线程,接着看useCommonPool这个是怎么逻辑判断的

 private static final boolean useCommonPool =(ForkJoinPool.getCommonPoolParallelism() > 1);

终于追到了源头:ForkJoinPool.getCommonPoolParallelism(),这个函数意义是什么意思,这一下到了我的知识盲区,果断去查找资料,在官方的文档中找到了对应的描述:docs.oracle.com/javase/8/do…

getCommonPoolParallelism
public static int getCommonPoolParallelism()
# 返回公共池的目标并行度级别。
Returns the targeted parallelism level of the common pool.
Returns:
# 公共池的目标并行度级别
the targeted parallelism level of the common pool
Since:
1.8

大体也就是根据系统的CPU相关信息来选择对应的线程池

在第二个函数中,我们传入的线程池,还对传入的线程池做了一个方法的判断:screenExecutor,大概就是帮助我们选择最优的线程池。

static Executor screenExecutor(Executor e) {
        if (!useCommonPool && e == ForkJoinPool.commonPool())
            return asyncPool;
        if (e == null) throw new NullPointerException();
        return e;
 }

接着就是两个函数的共同内部调用的方法:**asyncRunStage,**老规矩,先贴源码, 跟随源码一步步走 在AsyncRun中对我们的任务进行了包装执行

下面我们通过这两个方法的实际使用来找到对应的区别

  • 未设置Executor
 CompletableFuture.runAsync(() -> {
     System.out.println("子任务开始完毕");
         try {
             TimeUnit.SECONDS.sleep(3);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     System.out.println("子任务执行完毕");
});

执行结果:

main开始完毕
main执行完毕
子任务开始完毕
Process finished with exit code 0

从打印的结果可以看出,子任务还没有执行完毕,整个应用程序就终止了执行,出现的这种情况,只能说明commonPool中都是守护线程,主线程执行完,子线程也就是结束了,接下来我们看传入自定义的线程后,会发生怎样的情况

        ExecutorService executor = Executors.newFixedThreadPool(10);
        System.out.println("main开始完毕");
        CompletableFuture.runAsync(() -> {
            System.out.println("子任务开始完毕");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子任务执行完毕");
        },executor);
main开始完毕
main执行完毕
子任务开始完毕
子任务执行完毕

这个时候我们可以看到主线程池执行完成后,子线程也正常执行,但是又有个问题发生了,子线程执行完了,但是整个应用并没有结束,一直在执行中,这就跟我们传入的线程池相关,一直会让我们的执行挂起,不会结束整个应用。

supplyAsyn解析与应用

查看两个函数的源码:

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
	return asyncSupplyStage(asyncPool, supplier);
}  

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
	return asyncSupplyStage(screenExecutor(executor), supplier);
}

从源码中发现这与runAsync比较相试,只是共同调用的方法为:asyncSupplyStage

static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
	if (f == null) throw new NullPointerException();
	CompletableFuture<U> d = new CompletableFuture<U>();
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

asyncSupplyStage方法又与asyncRunStage比较相试,归根到底就是AsyncSupply这个类的实现区别 好了,到这里我们对异步任务创建的四个方法有了一定的认知,大体知道了底层的实现原理,我们这只是对异步任务进行了创建,但是这异步任务我们自己通过线程池自己都都可以创建异步任务,何必费这么多精力来创建异步任务。接下来我们就一起去分析对于异步任务执行完成后,触发回调及获取结果,这样我们可以进行后续的操作。