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实现相同的逻辑。
相关推荐
码事漫谈4 小时前
C++死锁深度解析:从成因到预防与避免
后端
码事漫谈4 小时前
智能体颠覆教育行业:现状、应用与未来展望调研报告
后端
蓝-萧4 小时前
【玩转全栈】----Django基本配置和介绍
java·后端
priority_key4 小时前
排序算法:堆排序、快速排序、归并排序
java·后端·算法·排序算法·归并排序·堆排序·快速排序
韩立学长4 小时前
基于Springboot的旧时月历史论坛4099k6s9(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
汤姆yu5 小时前
基于SpringBoot的动漫周边商场系统的设计与开发
java·spring boot·后端
灰小猿6 小时前
Spring前后端分离项目时间格式转换问题全局配置解决
java·前端·后端·spring·spring cloud
RedJACK~7 小时前
Go Ebiten小游戏开发:扫雷
开发语言·后端·golang
老夫的码又出BUG了7 小时前
分布式Web应用场景下存在的Session问题
前端·分布式·后端
L.EscaRC9 小时前
Spring Boot 自定义组件深度解析
java·spring boot·后端