JAVA并发编程——Future和CompletableFuture的简介与使用

1.Future类简介

2.Future类的使用

3.CompletableFuture类的简介

4.CompletableFuture类的的使用

5.总结

1.Future类简介
我们之前在这篇文章 JAVA并发编程——Callable接口和FutureTask简介和使用中说过,Future就是可以获得返回值的一个异步处理线程类。
在之前无论是继承Thread类还是实现Runnable接口,我们始终无法得到线程的返回值,但是Future就可以,Future是拥有返回值的线程,这样说我们可能还不明白,我们用一个生产的案例来说明一下这个例子。

2.Future类的使用
假设我们现在有这样一个需求:

我们先来看一个页面:
JAVA并发编程——Future和CompletableFuture的简介与使用_第1张图片

这是一个网上找来的数据大屏的图片,如果这个屏幕的数据要你查询出并且返回,你会怎么做?

我们知道大屏展示的区域不同,要查询的业务逻辑也就不一样,可以将它分成如下这几块区域:
JAVA并发编程——Future和CompletableFuture的简介与使用_第2张图片

如果现在是一名新手,可能会一个区域一个区域串行地去查询,区域一查完了查区域二,紧接着区域三,然后再返回给前端,这样效率不高。

改进方法:多个区域同步查询,最后统一返回给前端(多箭齐发)。

这么说还是比较抽象,我们用一张图片举例:
JAVA并发编程——Future和CompletableFuture的简介与使用_第3张图片

用这张图片应该就能解释了,接下来我们使用Future类进行代码展示一下:

public DataResult queryData() throws ExecutionException, InterruptedException, TimeoutException {
      
        DataResult result = new DataResult();
        //查询区域1
        FutureTask allHumanNum = new FutureTask<>(() -> queryHumanNum());
        //使用线程池执行
        CalculateThreadPoolConfig.getThreadPoolExecutor().submit(allDoctorNum);
        //查询区域2
        FutureTask allMaleNum = new FutureTask<>(() -> queryAllMaleNum());
       //使用线程池执行 CalculateThreadPoolConfig.getThreadPoolExecutor().submit(queryInvitedDoctorNum);
       //依次类推
       //......

        //拼接结果集
        result.setAllHumanNum(allHumanNum.get(5, TimeUnit.SECONDS));
        result.setAllMaleNum(allMaleNum.get(5, TimeUnit.SECONDS));
        return result;
    }

可以看出,我们无论查询多少个数据集,只用花费查询时间最长的那一个数据集的时间就够了,就实现了同步并行查询的效果,大大减少查询时间。

不过我们要注意一下这行代码:

allHumanNum.get(5, TimeUnit.SECONDS);

get的意思是一直阻塞返回结果,但是加了一个数字和单位就代表,只等待5秒,不然会抛异常。
我们工作的时候,如果需要调用别人的接口返回数据,但是不知道对方要计算多久,可能对方一个接口就把我们写的整个功能给拖垮。为了避免这种情况,工作中为了保护自己,我们需要学会加一层保险,如果对方的接口如果五秒没有返回就过时不候了。

3.CompletableFuture类的简介

介绍了前面的FutreTask类,我们知道这虽然是一个很好用的类,但是也是有很多的缺点的,比如:
1.get()容易阻塞
2.如果想要把多个结果集合并成一个结果集,就需要很多FutureTask
3.FutureTask只能用于单步的简单计算,不能将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。
4.当Future集合中某个任务最快结束时,返回结果。
等等

缺点还是比较多的,这个时候我们就可以使用 CompletableFuture 类!

CompletableFuture弥补了Future模式的缺点。它可以将多个结果集合并成一个结果集,又可以将一个异步任务分成好几个阶段进行计算,后面的结果又依赖之前的结果等等。

总而言之,FutureTask能办到的事,CompletableFuture也能办到,同时CompletableFuture还能办到更多的事情!

我们先搜索一下CompletableFuture这个类

JAVA并发编程——Future和CompletableFuture的简介与使用_第4张图片

它实现了Future接口和CompletionStage接口。

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

我们仔细看CompletionStage拥有的方法,根据thenApply,whenComplete,exceptionally等方法,可以大概看出这个类可以对一个计算的每个阶段进行编程,先执行完一步,(根据thenApply)再执行下一步,(whenComplete)最后运行完了会怎么样,(exceptionally)抛出异常会怎么样等等。

好像就像一条调用链一样,这就是所谓的链式编程。

JAVA并发编程——Future和CompletableFuture的简介与使用_第5张图片

接下来我们用一个案例用CompletableFuture进行实战。

4.CompletableFuture类的的使用

假设我们现在有一个电商比价需求:
我们要从各大网站上收集Think in java这本书的价格,开多个线程从各大网站收集,收集完之后输出信息到控制台,线程执行完毕也要输出信息,请使用CompletableFuture和链式编程来进行解决。

我们接下来开始编程:


public class CompletableFutureNextMallDemo {
 // 这是需要收集的价格网站
 static List list = Arrays.asList(
            new NetMall("jd"),
            new NetMall("pdff"),
            new NetMall("tmall"),
            new NetMall("xhs"),
            new NetMall("xy"),
            new NetMall("zh"),
            new NetMall("sf")
    );

}
class NetMall {
    // 这是网站的具体类以及计算价格的方法
    private String mallName;

    public NetMall() {
    }

    public NetMall(String mallName) {
        this.mallName = mallName;
    }

    public String getMallName() {
        return mallName;
    }

    public void setMallName(String mallName) {
        this.mallName = mallName;
    }

    //计算价格的方法
    public double calcPrice(String productName) {
        try {
            //假设这是计算过程
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }

}

最关键的执行方法

    //链式调度 多箭齐发
    public static List getPriceByASync(List list, String productName) {

        //循环遍历所有网站,每一个网站开启一个线程进行计算
        //CompletableFuture.supplyAsync代表有返回值的执行过程,相当于Future的get(0方法
        return list.stream().map(netMall -> CompletableFuture.supplyAsync(() -> {
            return String.format(productName + " in %s price is %.2f", netMall.getMallName(), netMall.calcPrice(productName));
        }
        //当第一步执行完毕后,第二步就是输出书的价格
        ).thenApply(r-> {
            System.out.println(r);
            return r;
        }
        //当完成任务之后,输出该网站计算完成
        ).whenComplete((v,e)-> {
            System.out.println(netMall.getMallName()+"计算完成!");
        }
        //当抛出异常的时候,输出异常
        ).exceptionally(e->{
            System.out.println("获取失败,出现异常!" );
            e.printStackTrace();
            return null;
        })).collect(Collectors.toList())
           .stream()
           //最后统一调用join方法返回结果
           .map(CompletableFuture::join)
           //最后合并成一个List,作为结果返回
           .collect(Collectors.toList());
    }

这里我们将CompletableFuture抽离出来会发现代码会是如下所示:

CompletableFuture
.supplyAsync()//执行有返回值的异步处理,相当于FutureTask的get()
.thenApply()//接着执行第二步
.whenComplete()//当完成时触发的逻辑
.exceptionally();//当出现异常的时候触发的逻辑

5.总结

通过今天的学习,我们了解了Future和CompletableFuture的基本理论和实践,这本身就是非常好用的两个类,希望我们在工作中能多多使用,不过也不要盲目使用多线程,因为多使用一种工具,就会多一份风险。

你可能感兴趣的:(JAVA并发编程——Future和CompletableFuture的简介与使用)