使用 CompletableFuture 提升多线程计算效率

309 阅读6分钟
恭喜DK焕发第二春,许秀永远滴神!

WechatIMG10919.jpg 在开发过程中,我们常常会遇到需要并行执行多个计算任务的场景,这些任务通常是相互独立的,最终我们需要将它们的结果合并成最终的输出。为了提高计算效率并减少响应时间,使用多线程并行执行这些计算任务是一个有效的方案。今天,我们将介绍如何使用 Java 8 中的 CompletableFuture 来简化异步操作的处理,提升多线程计算的效率,并使用合适的方式聚合这些结果。

为什么选择 CompletableFuture?

CompletableFuture 是 Java 8 引入的一个类,它使得异步编程变得更加简单和直观。相比传统的线程池或 Future,CompletableFuture 提供了更多的功能,如:

• 异步执行任务

• 支持任务链式处理

• 结果合并、异常处理、超时控制等

通过使用 CompletableFuture,我们可以避免回调地狱,同时保持代码简洁易懂。

解决方案

场景描述

假设我们有一个系统,需要并行处理多个独立的计算任务(例如,数据处理、文件读取等),并将它们的结果合并为最终输出。为了提高计算效率,我们需要使用线程池并发执行这些任务,之后将结果聚合。

使用 CompletableFuture 执行并行任务

首先,我们需要定义多个计算任务,并通过 CompletableFuture 异步执行这些任务。然后,我们将所有任务的结果合并为一个列表或其他类型的数据结构。

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class AsyncTaskExecutor {

    // 创建一个线程池
    private static final ExecutorService executorService = Executors.newFixedThreadPool(4);

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 假设我们有多个计算任务
        List<Callable<Integer>> tasks = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            tasks.add(() -> {
                // 模拟计算任务,返回任务ID的平方
                Thread.sleep(1000);
                return taskId * taskId;
            });
        }

        // 使用 CompletableFuture 并行执行任务
        List<CompletableFuture<Integer>> futures = tasks.stream()
                .map(task -> CompletableFuture.supplyAsync(() -> {
                    try {
                        return task.call();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return 0;
                }, executorService))
                .collect(Collectors.toList());

        // 聚合结果
        List<Integer> results = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                .thenApply(v -> futures.stream()
                        .map(CompletableFuture::join)
                        .collect(Collectors.toList()))
                .get();

        // 输出最终结果
        Integer finalResult = results.stream().mapToInt(Integer::intValue).sum();
        System.out.println("最终结果:" + finalResult);

        // 关闭线程池
        executorService.shutdown();
    }
}

代码解析

  1. 定义计算任务:每个任务通过 Callable 定义,模拟一个耗时操作(这里我们简单地返回任务 ID 的平方)。

  2. 线程池配置:我们使用 Executors.newFixedThreadPool() 创建一个固定大小的线程池,最大支持 4 个线程并行执行任务。

  3. 使用 CompletableFuture 执行任务:通过 CompletableFuture.supplyAsync() 异步执行每个计算任务,将每个任务的结果封装成 CompletableFuture 对象。

  4. 结果聚合:使用 CompletableFuture.allOf() 等待所有任务完成,然后通过 join() 方法获取每个任务的结果。最后,我们将所有结果合并,计算总和。

  5. 关闭线程池:使用 shutdown() 方法优雅地关闭线程池,防止资源泄露。

配置自定义线程池

不建议使用默认的线程池,我们可以自己配置一个,注入到容器中

@Configuration
public class ThreadPoolConfig {

    Integer corePoolSize = 4;
    Integer maximumPoolSize = 20;
    Integer keepAliveTime = 120;
    int POOL_SIZE = 1000;

    @Bean("forecastSkuSummaryThreadPool")
    public ThreadPoolExecutor stockThreadPool() {
        ThreadFactory threadFactory = new CustomizableThreadFactory("forecast-sku-summary-thread-pool ");
        LinkedBlockingQueue<Runnable> synchronousQueue = new LinkedBlockingQueue<>(POOL_SIZE);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, synchronousQueue, threadFactory);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

在这个配置中,我们定义了一个自定义线程池,并通过 @Bean 注解将其暴露到 Spring 容器中。通过这种方式,我们可以灵活地调整线程池的大小、线程的存活时间等参数,确保系统的性能和稳定性。

使用 CombinerFeature 聚合多线程结果

为了更好地管理多个任务的结果聚合,我们可以封装成一个通用的类 CombinerFeature,专门处理任务的合并逻辑。

import java.util.List;
import java.util.concurrent.*;

public class CombinerFeature<T, R> {
    private final ExecutorService executorService;
    private final List<Callable<T>> tasks;

    public CombinerFeature(ExecutorService executorService, List<Callable<T>> tasks) {
        this.executorService = executorService;
        this.tasks = tasks;
    }

    public R aggregate() throws InterruptedException, ExecutionException {
        List<Future<T>> futures = executorService.invokeAll(tasks);
        
        // 聚合所有任务的结果
        R result = null;
        for (Future<T> future : futures) {
            T partialResult = future.get();
            if (result == null) {
                result = (R) partialResult;
            } else {
                result = (R) combineResults(result, partialResult);  // 合并结果
            }
        }
        return result;
    }

    private R combineResults(R result, T partialResult) {
        // 聚合逻辑(根据业务逻辑调整)
        return (R) Integer.valueOf((Integer) result + (Integer) partialResult);
    }
}
1. aggregate()
  • 作用:聚合所有并发任务的结果。

  • 参数:无

  • 返回值:合并后的结果。

  • 异常InterruptedException, ExecutionException

    public R aggregate() throws InterruptedException, ExecutionException
    
2. combineResults(R result, T partialResult)

作用:合并两个任务的结果。

参数

• result: 当前已经合并的结果。

• partialResult: 本次计算任务的部分结果。

返回值:合并后的结果。根据实际情况,这个方法需要被重写以适应不同的合并逻辑。

private R combineResults(R result, T partialResult)
3. constructor

作用:构造 CombinerFeature 实例,初始化任务和执行器。

参数

• executorService: 任务执行的线程池。

• tasks: 需要并行执行的任务列表(Callable 任务)。

public CombinerFeature(ExecutorService executorService, List<Callable<T>> tasks)

常见用法示例

ExecutorService executorService = Executors.newFixedThreadPool(4);
List<Callable<Integer>> tasks = Arrays.asList(
    () -> 1,
    () -> 2,
    () -> 3
);

CombinerFeature<Integer, Integer> combinerFeature = new CombinerFeature<>(executorService, tasks);
Integer result = combinerFeature.aggregate();
System.out.println("合并后的结果:" + result);
需要重写的方法:
  1. combineResults:该方法默认是合并 Integer 类型的结果。如果你需要合并其他类型的结果,你可能需要重写该方法来实现不同的聚合逻辑。
其他可能扩展的 API(基于你的应用场景):

getResults() :返回所有任务的结果(不合并)。可用于调试或者查看每个任务的单独结果。

isCompleted() :检查所有任务是否完成。

cancelTasks() :取消所有未执行或正在执行的任务。

下面是一些CompletableFuture常用的方法,需要多个任务并行去处理的时候可以用到:

CompletableFuture方法

方法作用参数返回值
allOf()等待多个任务全部完成CompletableFuture<?>... cfsCompletableFuture<Void>
anyOf()等待多个任务中任意一个完成CompletableFuture<?>... cfsCompletableFuture<Object>
supplyAsync()异步执行一个没有参数的方法Supplier<U> supplierCompletableFuture<U>
runAsync()异步执行一个没有返回值的任务Runnable runnableCompletableFuture<Void>
thenApply()添加回调函数,在任务完成时执行操作并返回结果Function<? super T, ? extends U> fnCompletableFuture<U>
thenAccept()添加回调函数,在任务完成时执行操作但不返回结果Consumer<? super T> actionCompletableFuture<Void>
thenCombine()合并两个 CompletableFuture 的结果CompletableFuture<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fnCompletableFuture<V>
thenAcceptBoth()两个任务都完成时执行操作但不返回结果CompletableFuture<? extends T> other, BiConsumer<? super T, ? super T> actionCompletableFuture<Void>
whenComplete()任务完成后执行回调,处理结果或异常BiConsumer<? super T, ? super Throwable> actionCompletableFuture<T>
exceptionally()处理异常情况并返回默认值Function<Throwable, ? extends T> fnCompletableFuture<T>
handle()处理结果或异常BiFunction<? super T, Throwable, ? extends U> fnCompletableFuture<U>
join()等待任务完成并获取结果,如果异常抛出 CompletionExceptionT
get()等待任务完成并返回结果long timeout, TimeUnit unitT
get(long timeout, TimeUnit unit)等待任务完成并返回结果,支持超时long timeout, TimeUnit unitT

总结

在多线程计算场景中,使用 CompletableFuture 可以简化代码,提升计算效率。结合线程池的使用,我们可以实现高效的并行计算,并通过 CompletableFuture 对计算结果进行聚合和异常处理。CombinerFeature 类封装了任务的聚合逻辑,当然只是我个人业务场景为了方便累加计算所以这样实现了(处于隐私粗略盖了一下),大家个人用的时候按照CompletableFuture业务编排即可。

希望这篇博客对你有所帮助,如果有任何问题,欢迎留言讨论!