优雅使用 CompletableFuture 实现并行计算

优雅使用 CompletableFuture 实现并行计算

关于性能优化,我们通常划分为五大优化方向,有预计算、并行计算、异步计算、存储系统优化、其他算法优化。

在这里,我主要讲一下最常用的并行计算,其体现的思想是 “人多力量大,众人拾柴火焰高”,旨在通过将任务拆解后,以多路并行的方式,将任务执行的总时长进行缩短,以达到提升性能的目的。

在 java 中,要实现并行执行任务的话,离不开多线程并行利器 CompletableFutureallOf() 方法,下面主要来介绍CompletableFuture

1. 什么是 CompletableFuture

CompletableFuture 是 Java 提供的一个 Future 的增强版本,具有以下特点:

  • 非阻塞式编程:通过回调函数处理任务结果,而不是阻塞等待。
  • 丰富的组合能力:支持任务的串行和并行组合。
  • 异常处理:方便地处理异步任务中的异常。
  • 支持自定义线程池:通过显式线程池提高性能或定制化任务执行环境。

2. CompletableFuture 的基本使用

2.1 创建 CompletableFuture

可以通过以下方式创建:

CompletableFuture<String> future = new CompletableFuture<>();

但通常更常用的是工厂方法:

  • supplyAsync: 用于有返回值的异步任务。
  • runAsync: 用于无返回值的异步任务。

示例:

CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {
    return "Hello, CompletableFuture!";
});

CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> {
    System.out.println("Executing async task...");
});

2.2 任务的串行化

可以通过 thenApplythenAccept 等方法处理异步任务的结果。

示例:

CompletableFuture.supplyAsync(() -> "Task 1")
    .thenApply(result -> result + " -> Task 2")
    .thenAccept(System.out::println);

输出:

Task 1 -> Task 2

2.3 任务的并行组合

thenCombineallOf 可以用来组合多个任务。

示例:


package com.example.demo;

import java.util.concurrent.*;

public class Order {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Order order = new Order();
        order.checkOrder();
    }

    public boolean checkOrder() {

        if (!basicCheck()) {
            return false;
        }

        CompletableFuture<Boolean> checkRiskControl = CompletableFuture.supplyAsync(() -> {
            return true;
        });
        CompletableFuture<Boolean> checkCoupon = CompletableFuture.supplyAsync(() -> {
            return true;
        });
        CompletableFuture<Boolean> checkPoint = CompletableFuture.supplyAsync(() -> {
            return true;
        });
        CompletableFuture<Boolean> checkGoods = CompletableFuture.supplyAsync(() -> {
            return true;
        });
        CompletableFuture<Boolean> checkInventory = CompletableFuture.supplyAsync(() -> {
            return true;
        });

        CompletableFuture<Boolean> result = CompletableFuture.allOf(checkRiskControl, checkCoupon, checkPoint, checkGoods, checkInventory)
                .thenApply(res -> {
                    return checkRiskControl.join() && checkCoupon.join() && checkPoint.join() && checkGoods.join() && checkInventory.join();
                });

        System.out.println("订单完成前置校验结果为" + result.join());

        return true;
    }

    public boolean basicCheck() {
        return true;
    }
}

日志打印如下:

订单完成前置校验结果为true

2.4 异常处理

通过 exceptionallyhandle 处理异常:

CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Something went wrong");
    return "Success";
}).exceptionally(ex -> {
    System.out.println("Exception: " + ex.getMessage());
    return "Fallback";
}).thenAccept(System.out::println);

输出:

Exception: Something went wrong
Fallback

3. 自定义线程池

默认情况下,CompletableFuture 使用 ForkJoinPool.commonPool() 执行任务。但在一些场景中,例如需要更好的性能控制或隔离任务,可以使用自定义线程池。

3.1 创建线程池

根据任务类型的不同,可以选择合适的线程池配置。

CPU 密集型任务

对于 CPU 密集型任务,其线程池配置应尽量避免线程上下文切换,从而提升 CPU 的利用率。一般情况下,线程池的核心线程数和最大线程数建议设置为等于或略小于 CPU 核心数(Runtime.getRuntime().availableProcessors()),这样线程数刚好能充分利用 CPU,而不会因为过多线程竞争 CPU 而导致频繁上下文切换。因此,线程数通常设置为 NN+1,其中 N 是可用处理器核心数:

private final ExecutorService executorService = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(), // 核心线程数
        Runtime.getRuntime().availableProcessors(), // 最大线程数
        60L,                                        // 空闲线程最大存活时间
        TimeUnit.SECONDS,                           // 时间单位
        new LinkedBlockingQueue<>(),                // 阻塞队列,选择无界队列防止任务丢失
        Executors.defaultThreadFactory(),           // 默认线程工厂
        new ThreadPoolExecutor.AbortPolicy()        // 拒绝策略:抛出异常
);
IO 密集型任务

对于 IO 密集型任务,线程通常会因为等待 IO 操作(如网络请求、磁盘读写等)而处于阻塞状态,CPU 并不一直被占用。因此,IO 型任务的线程池通常需要更多线程以充分利用等待时间,从而提升吞吐量。因此,线程数通常设置为 2N4N,以充分利用线程的等待时间:

private final ExecutorService ioBoundExecutor = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors() << 1,
    Runtime.getRuntime().availableProcessors() << 2,
    60,
    TimeUnit.SECONDS,
    new SynchronousQueue<>(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

3.2 使用线程池

将线程池传递给 supplyAsyncrunAsync 方法:

CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " is executing the task");
    return "Result from custom thread pool";
}, ioBoundExecutor).thenAccept(System.out::println);

输出示例:

Custom-Executor-Thread is executing the task
Result from custom thread pool

3.3 关闭线程池

不要忘记在任务完成后关闭线程池:

ioBoundExecutor.shutdown();
cpuBoundExecutor.shutdown();

4. 串行与并行的对比

以下示例使用 CompletableFuture 实现并行任务和串行任务的对比,来体现并行计算的优势。

import java.util.concurrent.*;

public class CompletableFutureExample {

    public static void main(String[] args) throws Exception {
        // 自定义线程池
        ExecutorService customThreadPool = new ThreadPoolExecutor(
                Runtime.getRuntime().availableProcessors(), // 核心线程数
                Runtime.getRuntime().availableProcessors(), // 最大线程数
                60L,                                        // 空闲线程最大存活时间
                TimeUnit.SECONDS,                           // 时间单位
                new LinkedBlockingQueue<>(),                // 阻塞队列,选择无界队列防止任务丢失
                Executors.defaultThreadFactory(),           // 默认线程工厂
                new ThreadPoolExecutor.AbortPolicy()        // 拒绝策略:抛出异常
        );

        // 模拟任务
        Callable<Integer> task1 = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing task1");
            Thread.sleep(2000);
            return 1;
        };

        Callable<Integer> task2 = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing task2");
            Thread.sleep(3000);
            return 2;
        };

        Callable<Integer> task3 = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing task3");
            Thread.sleep(1000);
            return 3;
        };

        // 串行执行
        long startSerial = System.currentTimeMillis();
        int result1 = task1.call();
        int result2 = task2.call();
        int result3 = task3.call();
        long endSerial = System.currentTimeMillis();

        System.out.println("Serial Execution Result: " + (result1 + result2 + result3));
        System.out.println("Serial Execution Time: " + (endSerial - startSerial) + "ms\n");

        // 并行执行
        long startParallel = System.currentTimeMillis();

        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                return task1.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, customThreadPool);

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                return task2.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, customThreadPool);

        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            try {
                return task3.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, customThreadPool);

        CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2, future3);
        // 等待所有任务完成
        allOf.join();

        int parallelResult = future1.get() + future2.get() + future3.get();
        long endParallel = System.currentTimeMillis();

        System.out.println("Parallel Execution Result: " + parallelResult);
        System.out.println("Parallel Execution Time: " + (endParallel - startParallel) + "ms\n");

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

运行结果:

main is executing task1
main is executing task2
main is executing task3
Serial Execution Result: 6
Serial Execution Time: 6009ms

pool-1-thread-1 is executing task1
pool-1-thread-2 is executing task2
pool-1-thread-3 is executing task3
Parallel Execution Result: 6
Parallel Execution Time: 3012ms

运行后可以观察到,串行执行耗时较长,而并行执行通过并发显著缩短了时间。

5. 总结

通过 CompletableFuture 和自定义线程池,可以灵活优化并行任务的执行性能。

  • 并行任务通过多线程同时执行,总耗时取决于最慢任务,显著提升了性能。
  • 根据任务类型来选择合适的线程池配置,能够有效避免 cpu 资源浪费,并提高程序的整体吞吐量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Poldroc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值