77

我刚刚读完这篇文章:Java-5 ThreadPoolExecutor 与 Java-7 ForkJoinPool 相比有什么优势?并且觉得答案不够直截了当。

你能用简单的语言和例子解释一下,Java 7 的 Fork-Join 框架和旧的解决方案之间有什么权衡吗?

我还阅读了谷歌关于Java Tip: When to use ForkJoinPool vs ExecutorService from javaworld.com的 #1 热门文章,但这篇文章没有回答标题问题when,它主要谈论 api 差异......

4

6 回答 6

68

Fork-join 允许您轻松执行分而治之的作业,如果您想在ExecutorService. 在实践ExecutorService中,通常用于同时处理许多独立的请求(也称为事务),并在您想要加速一项连贯的工作时使用 fork-join。

于 2014-01-16T08:25:10.060 回答
44

Fork-join 特别适用于递归问题,其中任务涉及运行子任务然后处理它们的结果。(这通常被称为“分而治之”......但这并没有揭示基本特征。)

如果您尝试使用传统线程(例如通过 ExecutorService)解决这样的递归问题,您最终会遇到线程被捆绑等待其他线程向它们传递结果。

另一方面,如果问题不具备这些特征,则使用 fork-join 并没有真正的好处。


参考:

于 2014-01-16T09:17:42.450 回答
25

Java 8 在 Executors 中提供了另外一种 API

static ExecutorService  newWorkStealingPool()

创建一个工作窃取线程池,使用所有可用处理器作为其目标并行度级别。

加上这个 API,Executors提供了不同类型的ExecutorService选项。

根据您的要求,您可以选择其中之一,也可以寻找ThreadPoolExecutor,它可以更好地控制有界任务队列大小RejectedExecutionHandler机制。

  1. static ExecutorService newFixedThreadPool(int nThreads)

    创建一个线程池,该线程池重用在共享无界队列上运行的固定数量的线程。

  2. static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    创建一个线程池,可以安排命令在给定延迟后运行,或定期执行。

  3. static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

    创建一个线程池,根据需要创建新线程,但会在可用时重用以前构造的线程,并在需要时使用提供的 ThreadFactory 创建新线程。

  4. static ExecutorService newWorkStealingPool(int parallelism)

    创建一个线程池来维护足够的线程来支持给定的并行级别,并且可以使用多个队列来减少争用。

这些 API 中的每一个都旨在满足您的应用程序的相应业务需求。使用哪一个将取决于您的用例要求。

例如

  1. 如果要按到达顺序处理所有提交的任务,只需使用newFixedThreadPool(1)

  2. 如果您想优化递归任务的大型计算的性能,请使用ForkJoinPoolnewWorkStealingPool

  3. 如果您想定期或在将来的某个时间执行某些任务,请使用newScheduledThreadPool

再看一篇关于用例的好文章。PeterLawreyExecutorService

相关的 SE 问题:

java Fork/Join 池、ExecutorService 和 CountDownLatch

于 2016-01-21T11:12:08.303 回答
7

Fork-Join 框架是 Executor 框架的扩展,专门解决递归多线程程序中的“等待”问题。事实上,新的 Fork-Join 框架类都是从 Executor 框架的现有类扩展而来的。

Fork-Join 框架有两个核心特征

  • 工作窃取(空闲线程从队列中的任务超过当前可以处理的线程窃取工作)
  • 能够递归分解任务并收集结果。(显然,这个要求一定是随着并行处理概念的概念而出现的......但在 Java 7 之前缺乏可靠的 Java 实现框架)

如果并行处理需求是严格递归的,那只能选择 Fork-Join,否​​则 executor 或者 Fork-Join 框架都可以,虽然 Fork-Join 可以说是因为线程空闲,资源利用率更高从繁忙的线程中“窃取”一些任务。

于 2015-04-02T05:40:20.137 回答
7

Brian Goetz 最好地描述了这种情况:https ://www.ibm.com/developerworks/library/j-jtp11137/index.html

使用传统的线程池来实现 fork-join 也具有挑战性,因为 fork-join 任务的大部分时间都在等待其他任务。这种行为是线程饥饿死锁的秘诀,除非仔细选择参数来限制创建的任务数量或池本身是无限制的。传统的线程池是为相互独立的任务而设计的,并且在设计时也考虑到了潜在的阻塞、粗粒度任务——fork-join 解决方案两者都不产生。

我建议阅读整篇文章,因为它有一个很好的例子来说明为什么要使用分叉连接池。是在 ForkJoinPool 正式之前写的,所以coInvoke()他指的方法变成了invokeAll().

于 2018-05-26T17:36:37.923 回答
7

Fork Join 是 ExecuterService 的一个实现。主要区别在于此实现创建了 DEQUE 工作池。任务从一侧插入但从任何一侧撤回。这意味着如果您创建了new ForkJoinPool()它,它将寻找可用的 CPU 并创建那么多工作线程。然后,它将负载均匀地分布在每个线程上。但是如果一个线程工作缓慢而其他线程工作速度很快,他们就会从慢线程中挑选任务。从背面。以下步骤将更好地说明窃取。

第 1 阶段(最初):
W1 -> 5,4,3,2,1
W2 -> 10,9,8,7,6

第 2 阶段:
W1 -> 5,4
W2 -> 10,9,8,7,

第三阶段:
W1 -> 10,5,4
W2 -> 9,8,7,

而 Executor 服务创建请求数量的线程,并应用阻塞队列来存储所有剩余的等待任务。如果您使用了 cachedExecuterService,它将为每个作业创建一个线程,并且不会有等待队列。

于 2017-08-04T12:45:41.200 回答