9.3 调整线程池大小

问题

用户希望在通用线程池中自定义线程数量,而不是使用默认大小的线程池。

方案

适当调整系统参数,或将任务提交到自定义的 ForkJoinPool 实例。

讨论

根据 Javadoc 对 java.util.concurrent.ForkJoinPool 类的描述,可以通过以下三种系统属性控制通用线程池的构建:

  • java.util.concurrent.ForkJoinPool.common.parallelism
  • java.util.concurrent.ForkJoinPool.common.threadFactory
  • java.util.concurrent.ForkJoinPool.common.exceptionHandler

之前曾经提到过,默认情况下,通用线程池大小等于 JVM 上可用的处理器数量,它由 Runtime.getRuntime().availableProcessors() 确定。parallelism 标志用于指定并行级别(parallelism level),它是一个非负整数,可以通过编程方式或命令行方式来设置。例 9-12 显示了如何采用 System.setProperty 方法创建所需的并行级别。

例 9-12 采用编程方式设置通用线程池大小

  1. System.setProperty(
  2. "java.util.concurrent.ForkJoinPool.common.parallelism", "20");
  3. long total = LongStream.rangeClosed(1, 3_000_000)
  4. .parallel()
  5. .sum();
  6. int poolSize = ForkJoinPool.commonPool().getPoolSize();
  7. System.out.println("Pool size: " + poolSize);

➊ 打印 Pool size: 20

9.3 调整线程池大小 - 图1 将通用线程池大小设置为大于可用的 CPU 核心数无助于性能提升 11。

11关于不同并发实现的性能比较,Alex Zhitnitsky 在 Fork/Join Framework vs. Parallel Streams vs. ExecutorService: The Ultimate Fork/Join Benchmark 一文中做了深入讨论。——译者注

在命令行中,可以像使用任何系统属性一样使用 -D 标志。请注意,编程设置将覆盖命令行设置,如例 9-13 所示。

例 9-13 采用系统参数设置通用线程池大小

  1. $ java -cp build/classes/main concurrency.CommonPoolSize
  2. Pool size: 20
  3. // 注释掉System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
  4. $ java -cp build/classes/main concurrency.CommonPoolSize
  5. Pool size: 7
  6. $ java -cp build/classes/main \
  7. -Djava.util.concurrent.ForkJoinPool.common.parallelism=10 \
  8. concurrency.CommonPoolSize
  9. Pool size: 10

本例在一台 8 核计算机上运行。默认的线程池大小为 7,但如果将 main 线程包括在内,则默认情况下共有 8 个活动线程。

自定义ForkJoinPool

ForkJoinPool 类定义了一个构造函数,它传入一个整数作为并行级别。我们可以借此创建有别于通用线程池的自定义线程池,并将任务提交给它。

例 9-14 显示了如何创建自定义线程池。

例 9-14 创建自定义 ForkJoinPool

  1. ForkJoinPool pool = new ForkJoinPool(15);
  2. ForkJoinTask<Long> task = pool.submit(
  3. () -> LongStream.rangeClosed(1, 3_000_000)
  4. .parallel()
  5. .sum());
  6. try {
  7. total = task.get();
  8. } catch (InterruptedException | ExecutionException e) {
  9. e.printStackTrace();
  10. } finally {
  11. pool.shutdown();
  12. }
  13. poolSize = pool.getPoolSize();
  14. System.out.println("Pool size: " + poolSize);

❶ 实例化一个大小为 15 的 ForkJoinPool

❷ 提交 Callable 作为任务

❸ 执行任务并等待回复

❹ 打印 Pool size: 15

大部分情况下,在流上调用 parallel 方法时所用的通用线程池都能满足要求。如果需要改变其大小,请调整系统属性;如果仍然无法满足要求,请尝试创建自定义 ForkJoinPool 并将任务提交给它。

无论如何,在确定采用何种长期解决方案之前,请务必收集相关的性能数据。

另见

通过自定义线程池实现并行计算的另一种方案是采用 CompletableFuture,相关讨论请参见范例 9.5。