【问题标题】:Why is multi-threaded with CompletableFuture slow as compared to single threaded code?为什么 CompletableFuture 的多线程比单线程代码慢?
【发布时间】:2019-01-22 23:32:07
【问题描述】:

我正在尝试提高在单线程中运行的项目中当前代码的性能。代码正在做这样的事情: 1. 获取 10000000 个对象的第一个列表。 2. 获取 10000000 个对象的第二个列表。 3. 将这两个(经过一些更改)合并到第三个列表中。

   Instant s = Instant.now();
    List<Integer> l1 = getFirstList();
    List<Integer> l2 = getSecondList();
    List<Integer> l3 = new ArrayList<>();
    l3.addAll(l1);
    l3.addAll(l2);
    Instant e = Instant.now();
    System.out.println("Execution time: " + Duration.between(s, e).toMillis());

这里是获取和组合列表的示例方法

    private static List<Integer> getFirstList() {
    System.out.println("First list is being created by: "+ Thread.currentThread().getName());
    List<Integer> l = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
        l.add(i);
    }
    return l;
}

private static List<Integer> getSecondList() {

    System.out.println("Second list is being created by: "+ Thread.currentThread().getName());
    List<Integer> l = new ArrayList<>();
    for (int i = 10000000; i < 20000000; i++) {
        l.add(i);
    }
    return l;
}
private static List<Integer> combine(List<Integer> l1, List<Integer> l2) {

    System.out.println("Third list is being created by: "+ Thread.currentThread().getName());
   ArrayList<Integer> l3 = new ArrayList<>();
   l3.addAll(l1);
   l3.addAll(l2);
    return l3;
}

我正在尝试将上面的代码重写如下:

    ExecutorService executor = Executors.newFixedThreadPool(10);
    Instant start = Instant.now();
    CompletableFuture<List<Integer>> cf1 = CompletableFuture.supplyAsync(() -> getFirstList(), executor);
    CompletableFuture<List<Integer>> cf2 = CompletableFuture.supplyAsync(() -> getSecondList(), executor);

    CompletableFuture<Void> cf3 = cf1.thenAcceptBothAsync(cf2, (l1, l2) -> combine(l1, l2), executor);
    try {
        cf3.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    Instant end = Instant.now();
    System.out.println("Execution time: " + Duration.between(start, end).toMillis());

    executor.shutdown();

单线程代码的执行时间为 4-5 秒,而多线程代码的执行时间为 6 秒以上。我做错了吗?

【问题讨论】:

    标签: asynchronous java-8 completable-future


    【解决方案1】:

    您是第一次执行这些方法,因此它们以解释模式启动。为了加速它们的第一次执行,优化器必须在它们运行时替换它们(称为堆栈替换),这并不总是提供与重新输入优化结果时相同的性能。同时执行此操作似乎更糟糕,至少对于 Java 8,因为我在 Java 11 中得到了完全不同的结果。

    所以第一步是插入显式调用,例如getFirstList(); getSecondList();,看看它在第一次不被调用时的表现。

    另一个方面是垃圾收集。一些 JVM 从一个小的初始堆开始,每次扩展堆时都会执行一次完整的 GC,这对所有线程都有影响。

    所以第二步将从-Xms1G(甚至更好,-Xms2G)开始,为您要创建的对象数量设置合理的堆大小。

    但请注意,将中间结果列表添加到最终结果列表的第三步(在任何一种情况下都是按顺序发生的)对性能有很大影响。

    因此,第三步是用l3 = new ArrayList&lt;&gt;(l1.size() + l2.size()) 替换两个变体的最终列表的构造,以确保列表具有适当的初始容量。

    这些步骤的组合导致顺序执行不到一秒,Java 8 下的多线程执行不到半秒。

    对于 Java 11,它的起点要好得多,开箱即用只需要大约一秒钟,这些改进带来的加速效果不那么显着。看来这段代码的内存消耗要高得多。

    【讨论】:

      【解决方案2】:

      在单线程变体中,l3.addAll(l1); l3.addAll(l2); 从处理器缓存中获取l1l2 的元素(它们在执行getFirstListgetSecondList 时被放在那里)。

      在并行变体中,方法 combine() 在具有空缓存的不同处理器内核上运行,并从主内存中获取所有元素,这要慢得多。

      【讨论】:

        猜你喜欢
        • 2014-07-21
        • 1970-01-01
        • 1970-01-01
        • 2015-07-09
        • 2019-02-20
        • 2012-09-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多