CompletableFuture#allOf、依次 join、ListenableFuture#allAsList 的性能比较

引子

之前写过一篇《使用 CompletableFuture 最常见的错误(附实战代码)》,读者 闭关修炼啊哈 指出依次join的性能问题。结论在文章末尾。

Talk is cheap, show me your code! 参考读者 闭关修炼啊哈的实现,编写jmh基准测试代码。代码主要测试了三种实现,分别是CompletableFuture#allOf、依次 join、ListenableFuture#allAsList,任务为CPU密集型,设置不同大小的子任务,最终比较总的耗时。

java 复制代码
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ConcurrentDemoBenchmark {

    private List<Student> studentList;
    private ListeningExecutorService executor;
    @Param({"5", "10", "30"})
    private int numOfStudents;

    @Setup(Level.Iteration)
    public void setup() {
        studentList = IntStream.range(0, numOfStudents)
                .mapToObj(i -> new Student("学生" + i, i + 1))
                .collect(toList());
        int processors = Runtime.getRuntime().availableProcessors();
        executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(processors + 1));
    }

    @TearDown(Level.Iteration)
    public void tearDown() {
        executor.shutdownNow();
        System.out.println("shutdown");
    }

    @Benchmark
    public void testStreamJoin(Blackhole blackhole) {
        List<Integer> result = streamJoin(studentList, executor);
        blackhole.consume(result);
    }

    @Benchmark
    public void testCfAllOf(Blackhole blackhole) {
        List<Integer> result = cfAllOf(studentList, executor);
        blackhole.consume(result);
    }

    @Benchmark
    public void testLfAllOf(Blackhole blackhole) {
        List<Integer> result = lfAllOf(studentList, executor);
        blackhole.consume(result);
    }

    // 方法1:futureAllOf
    private static List<Integer> cfAllOf(List<Student> studentList, ListeningExecutorService executor) {
        @SuppressWarnings("unchecked")
        CompletableFuture<Integer>[] cfs = studentList.stream()
                .map(student -> CompletableFuture.supplyAsync(student::study, executor))
                .toArray(CompletableFuture[]::new);
        return CompletableFuture.allOf(cfs)
                .thenApply(__ -> Arrays.stream(cfs)
                        .map(CompletableFuture::join).collect(toList()))
                .join();
    }

    // 方法2:streamJoin
    private static List<Integer> streamJoin(List<Student> studentList, ListeningExecutorService executor) {
        return studentList.stream()
                .map(student -> CompletableFuture.supplyAsync(student::study, executor))
                .collect(toList())
                .stream()
                .map(CompletableFuture::join)
                .collect(toList());
    }

    // 方法3:LF
    private static List<Integer> lfAllOf(List<Student> studentList, ListeningExecutorService executor) {
        List<ListenableFuture<Integer>> futures = studentList.stream()
                .map(student -> executor.submit(student::study))
                .collect(toList());
        return Futures.getUnchecked(Futures.allAsList(futures));
    }

    public static class Student {
        private final String name;
        private final int sleep;

        public Student(String name, int sleep) {
            this.name = name;
            this.sleep = sleep;
        }

        public int study() {
//            long startTime = System.currentTimeMillis();
            double result = 0;
            for (int i = 0; i < 1_000_000 * sleep; i++) {
                result += Math.sqrt(result) + Math.sin(i);
            }
//            System.out.println(name + " study " + (System.currentTimeMillis() - startTime));
            return (int) result;
        }
    }
}

基准测试结果:

bash 复制代码
Benchmark                               (numOfStudents)  Mode  Cnt     Score    Error  Units
ConcurrentDemoBenchmark.testCfAllOf                   5  avgt   25   147.354 ±  1.696  ms/op
ConcurrentDemoBenchmark.testCfAllOf                  10  avgt   25   394.283 ±  3.566  ms/op
ConcurrentDemoBenchmark.testCfAllOf                  30  avgt   25  3391.347 ± 29.419  ms/op
ConcurrentDemoBenchmark.testLfAllOf                   5  avgt   25   146.789 ±  0.685  ms/op
ConcurrentDemoBenchmark.testLfAllOf                  10  avgt   25   394.108 ±  4.139  ms/op
ConcurrentDemoBenchmark.testLfAllOf                  30  avgt   25  3398.775 ± 39.469  ms/op
ConcurrentDemoBenchmark.testStreamJoin                5  avgt   25   146.795 ±  0.132  ms/op
ConcurrentDemoBenchmark.testStreamJoin               10  avgt   25   393.965 ±  4.783  ms/op
ConcurrentDemoBenchmark.testStreamJoin               30  avgt   25  3390.928 ± 26.038  ms/op

结论

  1. 三种实现性能差别不大,依次join性能更好一点。
  2. 选择哪种方法更多地取决于代码的可读性和具体的业务需求,而不是性能差异。
  3. 对于性能问题不要想当然,笔者之前错误地认为allOf实现依赖于回调,必然比多次同步等待性能要好,事实上,性能与依次join的性能差别不大,甚至差一点。
  4. 推荐使用fail-fast实现,其在出现错误时可以快速响应,拓展性更好。
  5. 还可以使用ExecutorService#invokeAll实现相同的逻辑。
相关推荐
㳺三才人子2 小时前
初探 Flask
后端·python·flask·html
星栈独行2 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.2 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易3 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶3 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl4 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel4 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记5 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒6 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰6 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程