实战分析Java的异步编程,并通过CompletableFuture进行高效调优

简介: 【6月更文挑战第7天】实战分析Java的异步编程,并通过CompletableFuture进行高效调优

一、写在开头

在我们一开始讲多线程的时候,提到过异步同步的概念,这里面我们再回顾一下:

  • 同步:调用方在调用某个方法后,等待被调用方返回结果;调用方在取得被调用方的返回值后,再继续运行。调用方顺序执行,同步等待被调用方的返回值,这就是阻塞式调用;
  • 异步:调用方在调用某个方法后,直接返回,不需要等待被调用方返回结果;被调用方开启一个线程处理任务,调用方可以同时去处理其他工作。调用方和被调用方是异步的,这就是非阻塞式调用。

适应场景
同步:如果数据存在线程间的共享,或竞态条件,需要同步。如多个线程同时对同一个变量进行读和写的操作,必须等前一个请求完成,后一个请求去调用前一个请求的结果,这时候就只能采用同步方式。
异步:当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就可以使用异步,提高效率、加快程序的响应。

而我们今天探讨的话题就是Java中的异步编程。

二、Future

为了提升Java程序的响应速度,在JDK1.5时引入了JUC包,里面包含了一个接口文件:Future,这是Java中实现异步编程的开端,我们可以将Future理解为一种异步思想或者一种设计模式;当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。

它的底层也是几个很容易理解的接口方法:

// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
   
   
    // 取消任务执行
    // 成功取消返回 true,否则返回 false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成
    boolean isDone();
    // 获取任务执行结果
    V get() throws InterruptedException, ExecutionException;
    // 指定时间内没有返回计算结果就抛出 TimeOutException 异常
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutExceptio
    }

这些接口大致提供的服务是:我有一个任务分配给了Future,然后我可以继续去干其他的事情,然后我可以在这个过程中去看任务是否完成,也可以取消任务,一段时间后我也可以去获取到任务执行后的结果,也可以设置任务多久执行完,没执行完抛异常等。

对于Future的使用,我想大家应该并不陌生的,我们在学习线程池的时候就有涉及,看下面这个测试案例:

//这里使用Executors只是方便测试,正常使用时推荐使用ThreadPoolExecutor!
ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<String> submit = executorService.submit(() -> {
   
   
    try {
   
   
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
   
   
        e.printStackTrace();
    }
    return "javabuild";
});
String s = submit.get();
System.out.println(s);
executorService.shutdown();

这里我们通过executorService.submit()方法去提交一个任务,线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值。

三、Future实战

经过了上面的学习了解,我们来根据案例场景进行实战使用Future,毕竟现在很多大厂除了问面试八股文之外,更多的会涉及到场景题!

场景模拟

假如你是一个12306的开发人员,为了在节假日满足大量用户的出行需要,请高效的完成:用户搜索一个目的地,推荐出所有的交通方案+酒店+耗时,并根据价格从低到高排序

拿到这种场景题的时候,我们往往需要分步处理:

  1. 根据目的地,搜索出所有的飞机、火车、客车路线,每个路线间隔30分钟;
  2. 计算出每种路线的耗时;
  3. 根据交通方案中最后一个到站点进行可用酒店匹配;
  4. 根据不同交通方案+对应的酒店价格进行最终出行总价格计算;
  5. 将所有组合的出行方案反馈给用户。

好了,分析完我们大概需要做的步骤,我们就来通过代码实现一下吧

第一步: 我们先来创建一个固定10个线程的线程池,用来处理以上每一步的任务。

//这里使用Executors只是演示,正常使用时推荐使用ThreadPoolExecutor!
ExecutorService executor = Executors.newFixedThreadPool(10);

第二步: 部分代码实例,方法就不贴了,太多太长了,大家需要对Future的用法理解即可

// 1. 根据传入的目的地查询所有出行方案,包括交通组合,价格,到站地点,出发时间,到站时间等
        Future<List<TripMethods>> tripMethods = executor.submit(() -> searchMethods(searchCondition));

        List<TripMethods> methods;
        try {
   
   
            methods = tripMethods.get();
        } catch (InterruptedException | ExecutionException e) {
   
   
            // 处理异常
        }

        // 2. 对每个出行方案的最终到站点查询酒店
        List<Future<List<Hotel>>> futureHotelsList = new ArrayList<>();
        for (TripMethods method : methods) {
   
   
            Future<List<Hotel>> futureHotels = executor.submit(() -> searchHotels(method));
            futureHotelsList.add(futureHotels);
        }
        // 出行方案=交通方案+酒店+耗时+价格
        List<Future<List<TravelPackage>>> futureTravelPackagesList = new ArrayList<>();
        for (Future<List<Hotel>> futureHotels : futureHotelsList) {
   
   
            List<Hotel> hotels;
            try {
   
   
                hotels = futureHotels.get();
            } catch (InterruptedException | ExecutionException e) {
   
   
                // 处理异常
            }

            // 3. 对每个交通方案的价格和其对应的酒店价格进行求和
            for (Hotel hotel : hotels) {
   
   
                Future<List<TravelPackage>> futureTravelPackages = executor.submit(() -> calculatePrices(hotel));
                futureTravelPackagesList.add(futureTravelPackages);
            }
        }

        List<TravelPackage> travelPackages = new ArrayList<>();
        for (Future<List<TravelPackage>> futureTravelPackages : futureTravelPackagesList) {
   
   
            try {
   
   
                travelPackages.addAll(futureTravelPackages.get());
            } catch (InterruptedException | ExecutionException e) {
   
   
                // 处理异常
            }
        }

        // 4. 将所有出行方案按照价格排序
        travelPackages.sort(Comparator.comparing(TravelPackage::getPrice));

        // 5. 返回结果
        return travelPackages;

我们在这里将每一步分任务,都作为一个future对象,处理完返回。但是这样会带来诸多问题,比如:我们调用future的get方法是阻塞操作,大大影响效率,并且在复杂的链路关系中,这种拆分式的写法,很难理清楚关联关系,先后关系等;

四、CompletableFuture 调优

在这种背景下,Java 8 时引入CompletableFuture 类,它的诞生是为了解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。

//CompletableFuture实现了Future的接口方法,CompletionStage 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
   
   
}

在CompletableFuture类中通过CompletionStage提供了大量的接口方法,他们让CompletableFuture拥有了出色的函数式编程能力,方法太多,我们无法一一讲解,只能通过对上面测试源码进行调优时,去使用,使用到的解释一下哈。
image.png

【CompletableFuture优化代码】

CompletableFuture.supplyAsync(() -> searchMethods())  // 1. 根据传入的目的地查询所有出行方案,包括交通组合,价格,到站地点,出发时间,到站时间等
                .thenCompose(methods -> {
   
     // 2. 对每个出行方案的最终到站点查询酒店
                    List<CompletableFuture<List<TravelPackage>>> travelPackageFutures = methods.stream()
                            .map(method -> CompletableFuture.supplyAsync(() -> searchHotels(method))  // 查询酒店
                                    .thenCompose(hotels -> {
   
     // 3. 对每个交通方案的价格和其对应的酒店价格进行求和
                                        List<CompletableFuture<TravelPackage>> packageFutures = hotels.stream()
                                                .map(hotel -> CompletableFuture.supplyAsync(() -> new TravelPackage(method, hotel)))
                                                .collect(Collectors.toList());

                                        return CompletableFuture.allOf(packageFutures.toArray(new CompletableFuture[0]))
                                                .thenApply(v -> packageFutures.stream()
                                                        .map(CompletableFuture::join)
                                                        .collect(Collectors.toList()));
                                    }))
                            .collect(Collectors.toList());

                    return CompletableFuture.allOf(travelPackageFutures.toArray(new CompletableFuture[0]))
                            .thenApply(v -> travelPackageFutures.stream()
                                    .flatMap(future -> future.join().stream())
                                    .collect(Collectors.toList()));
                })
                .thenApply(travelPackages -> {
   
     // 4. 将所有出行方案按照价格排序
                    return travelPackages.stream()
                            .sorted(Comparator.comparing(TravelPackage::getPrice))
                            .collect(Collectors.toList());
                })
                .exceptionally(e -> {
   
     // 处理所有的异常
                    // 处理异常
                    return null;
                });

在这里我们将整个实现都以一种函数链式调用的方式完成了,看似冗长,实则各个关系的先后非常明确,对于复杂的业务逻辑实现更加容易进行问题的排查与理解。

【解析】

1)在这段代码的开头,我们通过CompletableFuture 自带的静态工厂方法supplyAsync() 进行对象的创建,平时还可以用以new关键字或者runAsync()方法创建实例;

static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
// 使用自定义线程池(推荐)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
static CompletableFuture<Void> runAsync(Runnable runnable);
// 使用自定义线程池(推荐)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

2)thenCompose():用 thenCompose() 按顺序链接两个 CompletableFuture 对象,实现异步的任务链。它的作用是将前一个任务的返回结果作为下一个任务的输入参数,从而形成一个依赖关系。注意:这个方法是非阻塞的,即查询酒店的操作会立即开始,而不需要等待查询交通方案的操作完成。

3)thenApply():thenApply() 方法接受一个 Function 实例,用它来处理结果;

4)allOf() :方法会等到所有的 CompletableFuture 都运行完成之后再返回;

5) 调用 join() 可以让程序等待都运行完了之后再继续执行。

6)exceptionally():这个方法用于处理CompletableFuture的异常情况,如果CompletableFuture的计算过程中抛出异常,那么这个方法会被调用。

五、总结

好了,今天就讲这么多,其实在Java中通过条用CompletableFuture实现异步编排的工作还是稍微有点难度的,大量的API支持,需要我们在一次次的实战中去熟悉,并灵活使用。推荐大家去看看京东的asyncTool这个框架,里面就大量使用了CompletableFuture。

目录
相关文章
|
4天前
|
Java
【实战演练】JAVA网络编程高手养成记:URL与URLConnection的实战技巧,一学就会!
【6月更文挑战第22天】在Java网络编程中,理解和运用URL与URLConnection是关键。URL代表统一资源定位符,用于标识网络资源;URLConnection则用于建立与URL指定资源的连接。通过构造URL对象并调用openConnection()可创建URLConnection。示例展示了如何发送GET请求读取响应,以及如何设置POST请求以发送数据。GET将参数置于URL,POST将参数置于请求体。练习这些基本操作有助于提升网络编程技能。
|
3天前
|
前端开发 安全 Java
Java服务器端开发实战:利用Servlet和JSP构建动态网站
【6月更文挑战第23天】**Servlet和JSP在Java Web开发中扮演关键角色。Servlet处理业务逻辑,管理会话,JSP则结合HTML生成动态页面。两者协同工作,形成动态网站的核心。通过Servlet的doGet()方法响应请求,JSP利用嵌入式Java代码创建动态内容。实战中,Servlet处理数据后转发给JSP展示,共同构建高效、稳定的网站。虽然新技术涌现,Servlet与JSP仍为Java Web开发的基石,提供灵活且成熟的解决方案。**
|
3天前
|
缓存 负载均衡 安全
Servlet与JSP在Java Web应用中的性能调优策略
【6月更文挑战第23天】在Java Web中,Servlet和JSP调优至关重要,以应对高并发和复杂业务带来的性能挑战。优化包括Servlet复用、线程安全、数据库连接池,以及JSP的编译优化、使用JSTL、页面缓存和静态内容分离。全局优化涉及负载均衡、异步处理和缓存策略。通过这些实践,开发者能提升应用响应速度和吞吐量,确保高负载下的稳定运行。
|
1天前
|
算法 数据可视化 Java
Java中的性能调优技巧与工具推荐
Java中的性能调优技巧与工具推荐
|
1天前
|
SQL Java 数据库连接
Java持久化革命:Hibernate与JPA的实战应用
【6月更文挑战第25天】Java世界中,JPA作为ORM规范简化了数据库交互,而Hibernate是其实现者。通过引入依赖、定义注解实体类、配置 Hibernate,开发者能便捷地进行数据操作。Hibernate的使用减少了手动SQL的需求,提升了开发和维护效率,展示了其在持久化技术中的革命性影响和价值。
|
2天前
|
算法 Java 定位技术
基于Java的地震震中附近城市分析
基于Java的地震震中附近城市分析
5 0
|
2天前
|
SQL 监控 Java
技术实战:Java数据库连接池优化策略解析
【6月更文挑战第24天】Java应用的数据库连接池优化关键在于参数调整、预编译SQL和监控。案例中,高并发购物系统初期使用C3P0,因参数不合理导致性能瓶颈。调整如设置`MinPoolSize`为10,`MaxPoolSize`为50,和`CheckoutTimeout`为3000ms,配合预编译SQL提升执行效率。通过JMX监控连接池状态,动态调优,确保系统响应速度和资源利用。
|
2天前
|
Java 测试技术 Spring
Java反射包:java.lang.reflect的神奇功能与实战技巧
Java反射包:java.lang.reflect的神奇功能与实战技巧
|
5天前
|
缓存 负载均衡 安全
Java并发编程实战--简介
Java并发编程实战--简介
8 0
|
监控 Java
一篇搞定java调优的实战配置(下)
一篇搞定java调优的实战配置
71 0