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 小时前
Spring Boot + Vue 实现在线视频教育平台
vue.js·spring boot·后端
RunsenLIu3 小时前
基于Django实现的篮球论坛管理系统
后端·python·django
HelloZheQ5 小时前
Go:简洁高效,构建现代应用的利器
开发语言·后端·golang
caihuayuan55 小时前
[数据库之十四] 数据库索引之位图索引
java·大数据·spring boot·后端·课程设计
风象南6 小时前
Redis中6种缓存更新策略
redis·后端
程序员Bears7 小时前
Django进阶:用户认证、REST API与Celery异步任务全解析
后端·python·django
非晓为骁7 小时前
【Go】优化文件下载处理:从多级复制到零拷贝流式处理
开发语言·后端·性能优化·golang·零拷贝
北极象7 小时前
Golang中集合相关的库
开发语言·后端·golang
喵手7 小时前
Spring Boot 中的事务管理是如何工作的?
数据库·spring boot·后端
玄武后端技术栈9 小时前
什么是延迟队列?RabbitMQ 如何实现延迟队列?
分布式·后端·rabbitmq