Java CompletableFuture之异步任务

190 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

前言

在Java的一些业务常见当中,通常涉及到多任务的情况,例如在微服务当中,经常出现A调用B服务,B调用C之类的情况,如果是同步的线性调用的话,显然对于我们的业务并不友好。所以,为了提高我们的效率,我们需要使用到类似于前端的异步技术。在Java当中,通常可以使用CompletableFuture来实现。同时在微服务的调用中,也还会存在分布式的事务问题,当然这一部分就等到后面再聊聊。

异步任务启动

我们先来看看最简单的异步任务是如何启动的,首先我们最好先创建一个线程池,我们可以直接使用那7个参数的线程池进行创建,也可以直接创建一个newFixedThreadPool用来实验。但是在实际的开发当中还是实验那个7个参数的创建方法,之后是在SpringBoot当中自己写一个Bean注入或者使用配置@Bean注入自定义的线程池,然后去使用,我们这边就不多bibi了。

先来看到最简单的方式启动一个异步任务。

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) {
        System.out.println("main-----start----");
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("线程启动");
        }, executor);
        System.out.println("main-----end-----");

    }
}

我们使用CompletableFuture就启动了一个任务。

这个玩意有两个最基本的用法:

runAsync()
supplyAsync()

第一个就是咱们这里的,这个是没有返回值的。 第二个是有返回值的。

我们来个例子。

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程启动,有返回值");
            return 10;
        }, executor);

        System.out.println("main-----end-----"+"返回值为:"+future.get());

    }
}

这样一来我们可以实现最基本的操作,其实先前我们写过一个多线程的快速排序。当时用的是FutureTask,其实这个CompletableFuture 它其实就是用FutureTask来做的。但是为什么要用这个玩意呢,其实最重要的还是它CompletableFuture其实还具备了异步的编排能力,也就是说,我们可以指定异步的顺序。当然这个也可以有很多的方案自己手动实现,但是选择这个无疑是比较便捷的方案。

异步编排

成功处理

首先我们来看到这个代码:

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main-----start----");
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程启动,有返回值");
            return 10;
        }, executor).whenComplete((res,err)->{
            System.out.println("我拿到了结果是:"+res+"错误是:"+err);
        });

        System.out.println("main-----end-----"+"返回值为:"+future.get());
    }
}

我们使用这个whenComplete,这个效果其实和axios其实有点像,没错效果就是类似的。这里的话我们可以处理到res和err,但是的话,这里其实是成功后的处理,也就是和axios类似,我们还需要一个catch。 所以我们把代码这样处理:

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main-----start----");
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程启动,有返回值");
            return 10/0;
        }, executor).whenComplete((res,err)->{
            System.out.println("我拿到了结果是:"+res+"错误是:"+err);
        }).exceptionally((err)->{
            System.out.println(err.getMessage());
            return 10;
        });

        System.out.println("main-----end-----"+"返回值为:"+future.get());
    }
}

在这里插入图片描述

这个时候,你觉得太麻烦了,于是我们还可以直接这样。 我们使用handle

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main-----start----");
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程启动,有返回值");
            return 10/0;
        }, executor).handle((res,err)->{
            if(err!=null){
                System.out.println(err.getMessage());
                return 10;
            }else {
                return res;
            }
        });
        System.out.println("main-----end-----"+"返回值为:"+future.get());
    }
}

而且注意结果,你会发现,future.get() 拿到的其实最后处理到的结果。

串行化

都说了我们接下来的业务当中,可能是多个服务,例如A,调用B,B调用C,我总不能傻了吧唧的等A->B>C吧,我直接A,B,C同时执行,然后组装消息就ok了。

所以我们这里使用then开头的方法。 在这里插入图片描述

这里有个口诀,那就是以Async结尾的是咱们的异步,反之就是顺序串行同步调用。 中间有Accept的是可以接收上一个任务的值,但是没有返回值的,Apply是可以接受并且都有返回值的。 一般来说咱们远程的微服务调用用这个可能多一点,当然像订单发送,邮箱发送之类的,如果可以的话我还是喜欢上rabbitMQ。


public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main-----start----");
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务0启动,有返回值");
            return 10/0;
        }, executor).handle((res,err)->{
            if(err!=null){
                System.out.println(err.getMessage());
                return 10;
            }else {
                return res;
            }
        }).thenApplyAsync((res)->{
            return res*2;
        },executor);

        System.out.println("main-----end-----"+"返回值为:"+future.get());
    }
}

在这里插入图片描述

这里就很nice了,和前面的区别是啥呢,这是两个完全不同的任务。

任务组合

ok,刚刚显然是有一点随便,所以的话,我们拆分出两个任务来看看。 于是我们的代码变成了这样:

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main-----start----");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            return "task1";
        },executor);
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            return "task2";
        },executor);
        CompletableFuture<String> future3 = future1.thenCombineAsync(future2, (res1, res2) -> {
            return res1 + "---" + res2;
        }, executor);


        System.out.println("main-----end-----the future res:"+future3.get());
    }
}

在这里插入图片描述 这里的效果显然就是说,当我们的两个任务都执行完毕之后的话,我们的task3完成一个整合。那么消耗的时间的话,其实也是最长的任务时间, 例如我们将任务加上延时: 在这里插入图片描述

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("main-----start----");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task1";
        },executor);
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task2";
        },executor);
        CompletableFuture<String> future3 = future1.thenCombineAsync(future2, (res1, res2) -> {
            return res1 + "---" + res2;
        }, executor);
        System.out.println("main-----end-----the future res:"+
                future3.get()+"运行时间为"+(System.currentTimeMillis()-start)
        );
    }
}

那么这个别有啥用呢,这个显然是,咱们两个任务都结束了之后在执行的方法,但是有些时候的话可能两个方法只需要有一个执行完了就好了,那么我们这个时候就需要使用到这个方法了:

在这里插入图片描述 这个是没有返回值的。 如果我们需要返回的话,我们就使用这个在这里插入图片描述

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("main-----start----");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task1";
        },executor);
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task2";
        },executor);
        CompletableFuture<String> future3 = future1.applyToEitherAsync(future2, (res) -> {
            return "最先得到的结果是:"+res;
        }, executor);
        System.out.println("main-----end-----the future res:"+
                future3.get()+"运行时间为"+(System.currentTimeMillis()-start)
        );
    }
}

在这里插入图片描述

统一处理

这个其实没啥,名字是我瞎取的,因为我也不知道如何取描述要好一点。所以就直接这样叫吧,那么这个有啥作用嘛,其实没啥,就是好看,例如这样的代码,我们有多个任务,需要全部执行完毕,一般来说,我们可以通过调取get方法来让进行堵塞,这个也是为啥我们刚刚的计时的代码要和get方法在一个输出方法里面了。

我们来看到这个代码就懂了。

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("main-----start----");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            return "task1";
        },executor);
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            return "task2";
        },executor);
        CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务3开始");
            System.out.println("任务3结束");
            return "task2";
        },executor);

        CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2, future3);
        System.out.println(future1.get());
        System.out.println(future2.get());
        System.out.println(future3.get());
        System.out.println("main-----end-----");
    }
}

那么同样的我们也有只要有一i给任务结束就ok了的。

public class Test {
    public static ExecutorService executor = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("main-----start----");
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            return "task1";
        },executor);
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            return "task2";
        },executor);
        CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务3开始");
            System.out.println("任务3结束");
            return "task2";
        },executor);

        CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2, future3);
        System.out.println(any.get());
        System.out.println("main-----end-----");
    }
}

在这里插入图片描述 这里的话其实也可以发现他们都是异步启动的,所有的话,虽然我们拿到的是最先得到的结果,但是的话,其他的线程并没有因此停止,而是依然会等待执行完毕让后释放。

总结

以上内容就是这些,还是非常实用 的一些内容。尤其做一些查询的时候可以这样处理一下啊,众所周知,Java如果不是19的话是不支持协程,不像python人家虽然性能不咋地,而且只能跑一个核,但是人家可以异步啊,而且只能跑一个核,但是可以开多进程啊。