默认的ForkJoinPool执行器需要很长时间

时间:2017-08-02 12:12:40

标签: java-8 executorservice threadpoolexecutor completable-future forkjoinpool

我正在使用CompletableFuture来异步执行从列表源生成的流。

所以我正在测试重载的方法,即" supplyAsync" CompletableFuture,其中一个方法仅采用单个供应商参数,另一个采用供应商参数和执行者参数。 以下是两者的文档:

一个

  

supplyAsync(供应商供应商)

     

返回一个新的CompletableFuture,它由ForkJoinPool.commonPool()中运行的任务异步完成,其值通过调用给定的Supplier获得。

第二

  

supplyAsync(供应商供应商,执行人执行人)

     

返回一个新的CompletableFuture,它由在给定执行程序中运行的任务异步完成,其值通过调用给定的Supplier获得。

这是我的测试课:

public class TestCompleteableAndParallelStream {

    public static void main(String[] args) {
        List<MyTask> tasks = IntStream.range(0, 10)
                .mapToObj(i -> new MyTask(1))
                .collect(Collectors.toList());

        useCompletableFuture(tasks);

        useCompletableFutureWithExecutor(tasks);

    }

    public static void useCompletableFutureWithExecutor(List<MyTask> tasks) {
          long start = System.nanoTime();
          ExecutorService executor = Executors.newFixedThreadPool(Math.min(tasks.size(), 10));
          List<CompletableFuture<Integer>> futures =
              tasks.stream()
                   .map(t -> CompletableFuture.supplyAsync(() -> t.calculate(), executor))
                   .collect(Collectors.toList());

          List<Integer> result =
              futures.stream()
                     .map(CompletableFuture::join)
                     .collect(Collectors.toList());
          long duration = (System.nanoTime() - start) / 1_000_000;
          System.out.printf("Processed %d tasks in %d millis\n", tasks.size(), duration);
          System.out.println(result);
          executor.shutdown();
        }

    public static void useCompletableFuture(List<MyTask> tasks) {
          long start = System.nanoTime();
          List<CompletableFuture<Integer>> futures =
              tasks.stream()
                   .map(t -> CompletableFuture.supplyAsync(() -> t.calculate()))
                   .collect(Collectors.toList());

          List<Integer> result =
              futures.stream()
                     .map(CompletableFuture::join)
                     .collect(Collectors.toList());
          long duration = (System.nanoTime() - start) / 1_000_000;
          System.out.printf("Processed %d tasks in %d millis\n", tasks.size(), duration);
          System.out.println(result);
        }



}


class MyTask {
      private final int duration;
      public MyTask(int duration) {
        this.duration = duration;
      }
      public int calculate() {
        System.out.println(Thread.currentThread().getName());
        try {
          Thread.sleep(duration * 1000);
        } catch (final InterruptedException e) {
          throw new RuntimeException(e);
        }
        return duration;
      }
    }

而&#34; useCompletableFuture&#34;方法需要大约4秒才能完成,&#34; useCompletableFutureWithExecutor&#34;方法只需1秒即可完成。

我的问题不在于,ForkJoinPool.commonPool()可以进行哪些不同的处理?在那不应该我们总是更喜欢自定义执行器池而不是ForkJoinPool?

2 个答案:

答案 0 :(得分:4)

检查ForkJoinPool.commonPool()尺寸。默认情况下,它会创建一个大小为

的池
Runtime.getRuntime().availableProcessors() - 1

我在我的Intel i7-4800MQ(4核+ 4个虚拟核)上运行您的示例,在我的情况下,公共池的大小为7,因此整个计算需要大约2000毫秒:

ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-1
Processed 10 tasks in 2005 millis
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

在第二种情况下,您使用了

Executors.newFixedThreadPool(Math.min(tasks.size(), 10));

所以池有10个线程可以执行计算,因此所有任务都在~1000毫秒内运行:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10
Processed 10 tasks in 1002 millis
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

ForkJoinPoolExecutorService

之间的差异

Eugene在他的评论中提到了一个更重要的事情。 ForkJoinPool使用工作窃取方法:

  

ForkJoinPool与其他类型的ExecutorService的不同之处主要在于使用工作窃取:池中的所有线程都尝试查找和执行提交到池的任务和/或由其他活动任务创建的任务(最终阻塞)等待工作,如果不存在的话)。当大多数任务产生其他子任务时(如大多数ForkJoinTasks),以及从外部客户端向池提交许多小任务时,这可以实现高效处理。特别是在构造函数中将asyncMode设置为true时,ForkJoinPools也可能适用于从未加入的事件样式任务。

而使用.newFixedThreadPool()创建的ExecutorService使用分而治之的方法。

如何确定池大小?

有一个关于什么是最佳线程池大小的问题,您可以在那里找到有用的信息:

  

Setting Ideal size of Thread Pool

此线程也是一个值得研究的好地方:

  

Custom thread pool in Java 8 parallel stream

答案 1 :(得分:3)

进一步检查互联网上的解决方案,我发现我们可以使用以下属性更改ForkJoinPool采用的默认池大小:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

因此,这个属性可以进一步帮助使用更高效的方式和更多的并行性来使用ForkJoinPool。