并发编程实战——CompletableFuture两种使用场景

4,179 阅读3分钟

一、jdk并发类库

1.1 Future

Future的缺陷是没有办法异步化。
实际上他就只是获取线程的执行结果与执行状态,当需要进一步处理时,主线程需要调用get()方法。
但调用get()方法的时机很难把控,立即调用相当于串行了,很久之后调用那就白白浪费了等待的时间。

1.2 CompletableFuture

在jdk1.8,java推出了CompletableFuture。
在异步任务完成后,需要用到任务的返回结果时无需等待,直接通过thenAccept、thenApply、thenCompose等方法来对异步调用的结果进行处理。

二、业务场景

目前项目中应用到并发的场景主要是调用远程RPC接口,即IO密集型的场景。

  • 第一个场景是需要分别调用两个RPC接口,无先后顺序关系,数据处理是需要同时依赖两个接口的返回数据。
  • 第二个场景是需要循环调用RPC接口,调用100次以内,所有调用结束后统一对返回结果进行处理。

三、具体实现

3.1 两个接口调用后处理数据

  • supplyAsync 用来执行异步操作,可以支持返回值。
    没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。
  • thenCombineAsync thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
  • join CompletableFuture类中的join方法和Future接口中的get有相同的含义,等待运行结束。
    public List<String> findIdList(String subjectId,Integer classType) {
        CompletableFuture<List<String>> aIds = CompletableFuture
                .supplyAsync(() -> clientA.queryClassIdList("1", classType), TestThreadExecutor.getInstance());

        CompletableFuture<List<String>> bIds = CompletableFuture
                .supplyAsync(() -> clientB.queryClassIdList("2", classType), TestThreadExecutor.getInstance());

        CompletableFuture<List<String>> unionClassIds = aIds
                .thenCombineAsync(bIds, (aIds, bIds) -> {
                    aIds.addAll(bIds);
                    return aIds.stream().distinct().collect(Collectors.toList());
                }, TestThreadExecutor.getInstance());

        return unionClassIds
                .exceptionally(exception -> {
                    System.out.println("出异常了");
                    return List.of();
                })
                .join();
    }

3.2 循环调用同一个接口

  • allOf allOf里面的所有线程未执行完毕,主线程会阻塞,直到allOf里面的所有线程都执行,线程就会被唤醒。
List<String> res = new ArrayList<>();
List<String> idList = Lists.of("1","2","3","4","5","6");
              CompletableFuture[] cfs = idList.stream().map(object-> CompletableFuture.supplyAsync(()->client.queryClassIdList(object, classType), TestThreadExecutor.getInstance())
                      .thenApply(dtoList->{
                          if(CollectionUtils.isEmpty(dtoList)){
                              return "";
                          } else {
                              return claCourseId;
                          }
                      })
                      //如需获取任务完成先后顺序,此处代码即可
                      .whenComplete((v, e) ->                        
                      	  if(!StringUtils.isEmpty(v)){
                              bindClassCourseList.add(v);
                          }
                      })).toArray(CompletableFuture[]::new);
              //等待总任务完成,但是封装后无返回值,必须自己whenComplete()获取
              CompletableFuture.allOf(cfs).join();

3.3 自定义线程池

实际开发中我们要根据具体情况来自定义线程池,这里提供一个大概的模板。

public class TestThreadExecutor {

    /**
     * 线程池名称
     */
    private static final String TEST_THREAD_POOL = "teacher-city-code-thread-pool";

    /**
     * cpu可用核数
     */
    private static final int DEFAULT_CPU_PROCESSORS = Runtime.getRuntime().availableProcessors();

    /**
     * spring支持的线程池任务包装类
     */
    private static final ThreadPoolTaskExecutor TEST_THREAD_EXECUTOR = new ThreadPoolTaskExecutor();

    /**
     * 默认线程池
     */
    private static final ThreadFactory DEFAULT_THREAD_FACTORY = new DefaultThreadFactory(TEST_THREAD_POOL);

    static {
        TEST_THREAD_EXECUTOR.setThreadFactory(DEFAULT_THREAD_FACTORY);
        //此线程池负责业务属于IO密集型,设置核心线程数为cpu核数*2
        TEST_THREAD_EXECUTOR.setCorePoolSize(DEFAULT_CPU_PROCESSORS * 2);
        TEST_THREAD_EXECUTOR.setMaxPoolSize(DEFAULT_CPU_PROCESSORS * 25);
        TEST_THREAD_EXECUTOR.setQueueCapacity(1024);
        //默认值就是60seconds,显示声明的目的是直观看到线程池中线程的存活时间
        TEST_THREAD_EXECUTOR.setKeepAliveSeconds(60);
        //CallerRunsPolicy这个拒绝策略代表的是,如果工作列队满了(超过QueueCapacity)同时MaxPoolSize也达到了阀值,那么当前任务使用主线程调用
        TEST_THREAD_EXECUTOR.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //核心线程如果超过了keepTimeOut时间也会进行回收,设置这个参数的原因是因为其他maven-module可能不需要依赖这个线程池,从而造成线程资源的浪费
        TEST_THREAD_EXECUTOR.setAllowCoreThreadTimeOut(true);
        TEST_THREAD_EXECUTOR.initialize();
    }

    /**
     * 获取线程池任务实例
     *
     * @return ThreadPoolTaskExecutor
     */
    public static ThreadPoolTaskExecutor getInstance() {
        return TEST_THREAD_EXECUTOR;
    }
}

四、参考资料