JUC之CompletableFuture

该博客介绍了如何使用CompletableFuture来提升程序性能,通过并发处理商品详情页中的非关键信息,如图片查询,以减少总耗时。CompletableFuture不仅实现了Future接口,还提供了更强大的异步编排能力,允许在任务执行完后触发其他阶段的任务。在处理商品价格和优惠券价格的场景中,CompletableFuture能够避免阻塞,并精确控制超时,提高了系统响应速度。

CompletableFuture

需求

需求介绍

现在,需要实现一个商城的详情页,就如同京东、淘宝一样。在这个页面中,有一些不是行必要的信息和关键信息不是防止一个业务模块中的

如:商品的基本信息是放入shoping数据库,由商品的业务模块进行处理,商品介绍中的信息如介绍图片保存在image数据库中,由图片模块进行处理

需求解决

首先,可以使用基本是 思想对业务进行处理,即按照基本业务进行处理

两个基本的方法代表两个业务:查询商品和查询图片

public static void shopping(){
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("查询到了商品的详细信息");
}
public static void image(){
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("远程调用:查询到了商品图片的信息");
}

其中,远程调用因为网络的问题,所有需要的时间比本地调用的时间要长一些

public static void main(String[] args) {
    
    long startTime = System.currentTimeMillis();
    Main.shopping();
    Main.image();
    long endTime = System.currentTimeMillis();
    System.out.println("花费的时间:"+(endTime-startTime));
}

运行程序,所花费的时间是3s

运行结果:

查询到了商品的详细信息
远程调用:查询到了商品图片的信息
花费的时间:3024

需求 ->> 性能

解决 了基本的需求,现在需要将业务进行从需求到性能的改变。即如何能将这3s的时间进一步的缩短呢?我们可以做如下的思考

  • 两个业务,一个业务是1s,一个业务是2s,这两个业务需求的时间是不变的。
  • image的业务是非必要的,即如果调用偶尔超时,对整个业务来说是没什么影响的

以上两点,我们发现了一种解决的方式:新开一个线程,让这个线程去执行查询图片的业务,可以给这个线程定义一个超时时间,若在超时,则报一个异常。

Future

Future是在jdk1.5 的时候,JUC大神Doug Lea编写的一个用于异步计算和编排的一个接口。Future是一个接口,它由一个本源的实现类FutureTask

private static void m3() throws Exception {
    FutureTask<Integer> futureTask = new FutureTask<>(() -> {
        Main.image();
        return 1;
    });
    long startTime = System.currentTimeMillis();
    // 开另外一个线程去执行查询图片,因为查询顺序执行的,所以如果把这防止shopping后面,
    // 则这个线程会让main停止1s后才去执行,体现不出效果
    new Thread(futureTask,"t1").start();
    Main.shopping();
    // 程序在这个两秒中后不返回结果,则抛出异常
    // 注意:程序在前面就已经开始执行了,所以到这个get()的时候,程序已经执行了1s左右了
    // 所以,虽然这里是2s,但是不会超时
    futureTask.get(2L, TimeUnit.SECONDS);
    long endTime = System.currentTimeMillis();
    System.out.println("花费的时间:"+(endTime-startTime));
}

程序的执行结果如下

FutureTask的执行结果

查询到了商品的详细信息
远程调用:查询到了商品图片的信息
花费的时间:2017

好了,现在程序的性能已经从3s -> >> 2s了,从业务的最短时间来说,2s已经是完成业务需要的最短时间了,因为本地线程的1s和新线程的1s是并行执行的。

缺点:

会调用get方法造成线程的阻塞,虽然由一个超时的get方法,但是这个超时不是线程的执行时间,而是调用get方法后的超时时间

业务升级

新的需求:

由于用户量的提升,新增了优惠劵、促销等活动。若商品的价格能够触发优惠卷的 话,展现出来的价格就是使用了优惠劵之后的价格。

优惠劵的业务模块是一个单独的业务模块,需要使用微服务调用

对应这个新的需求,如果使用FutureTask来做的话,需要考虑如下问题:

  • 要将商品价格和优惠劵价格进行比较,得到一个显示的价格
  • 商品和优惠劵都是必要的信息,和图片信息不一样
  • 使用FutureTask的话,线程的超时时间不好控制,因为调用get方法可以传入一个超时时间,但是也会阻塞,而且超时时间是从调用get方法的时候开始计算的,即线程前面运行了多少时间无法确认,所以也无法在后面调用get方法

若要解决以上问题,我们想到了一个方法去解决:

  • 用一个线程去执行优惠劵的信息,再用另外一个线程去查询价格信息,再得到两个信息都完成后,对两者进行运算
  • 运算结束后将结果返回给主线程
  • 严格的控制超时时间

这就引入了jdk1.8的新方法CompletableFuture

CompletableFuture

介绍

先看类图

在这里插入图片描述

可以看出来,CompletableFuture是实现了Future的,所以它由Future的全部东南,并且还实例了另外一个接口:ComPletionStage,所以它的功能比Future更加的强大。这也符合设计模式中的开闭原则:扩展新类而不是修改旧类

简要的介绍一些CompletionStage接口:

  • 在一个阶段的任务执行完后可以触发另外一个阶段任务的执行,类是与Linux的管道分隔符 |
  • 提供了函数式编程的能力
  • 一个阶段的执行可以被单个阶段所触发,也可以被多个阶段触发

模拟

用一下代码来解决下上面的需求

private static void m4() throws Exception {
        // map模拟展示数据的vo;数值1表示从数据库中查询出的优惠劵;数值2表示查询商品的详细详细
        Map<Integer,Integer> map = new HashMap<>();
    // 新开一个线程了,这个线程暂停2s
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("查询结果为1");
            return 1;
        }).whenComplete((v, e) -> {
            // 当上面的运行完后,没有异常则将数据存入map中
            if (e == null) {
                System.out.println("查询成功");
                map.put(1, v);
            }
        }).exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
		// 主线程中向map中添加数据,暂停1s
        Main.put(map);
    	// 将future阻塞,保证主线程不终止
        future.get();
        System.out.println(map.get(2) - map.get(1));
    }

主线程的添加方法,暂停1s

    public static Map<Integer,Integer> put(Map<Integer,Integer> map){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(2,2);
        return map;
    }

大家发现了,在主线程中也含有get(),这个get也是和Future的get一样会造成阻塞,那么CompletableFuture的优点在哪呢?

大家注意链式编程中的whenComplete方法,这个方法是:当前面运行完后,有了结果就执行这个方法,两个参数值:返回结果和异常

此时,将whenComplete中的方法修改一下,就可以看出优点了:

if (e == null) {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException x) {
        x.printStackTrace();
    }
    System.out.println("查询成功");
    map.put(1, v);
}

这里加了一个1s的延迟,执行结果如下:

查询结果为1
查询成功
两者相减结果:1
执行时间==2099

若使用Future,则需要3s左右的时间

在这里插入图片描述

主要方法

在这里插入图片描述
在这里插入图片描述

关于thenComposethenApply的区别:

两者都是让A执行完后执行B,不同的是

这是thenApply的代码

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 100;
        });
        CompletableFuture<CompletableFuture<String>> f = future.thenApply(i -> {
            return CompletableFuture.supplyAsync(() -> {
                return (i * 10) + "";
            });
        });
        System.out.println(f.get().get()); //"1000"

这是thenCompose的代码

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 100;
});
CompletableFuture<String> f = future.thenCompose( i -> {
    return CompletableFuture.supplyAsync(() -> {
        return (i * 10) + "";
    });
});
System.out.println("main执行结束");
System.out.println(f.get()); //1000

可以看出,这两个的主要区别是返回的对象不同

  • thenCompose:偏向于对应之前得到的结果,再对其进行异步编排,需要再次使用到CompletableFuture,而得到的结果可以变成一个新的CompletableFuture返回
  • thenApply:偏向于对上面得到的结果进行处理,不用使用CompletableFuture

参考链接

总结

CompletableFuture在可以使用到一些从多处获取数据的地方使用,从而提高程序性能。

附录

函数式编程

接口名称方法名称参数返回值
Runnablerun无参数无返回值
Functionapply1个参数有返回值
Consumeaccept1个参数无返回值
Supplierget没有参数有返回值
据的地方使用,从而提高程序性能。

附录

函数式编程

接口名称方法名称参数返回值
Runnablerun无参数无返回值
Functionapply1个参数有返回值
Consumeaccept1个参数无返回值
Supplierget没有参数有返回值
BiConsumeraccept2个参数无返回值
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值