Java 的 Fork/Join vs ExecutorService-什么时候使用哪个?

我刚读完这篇文章: Java-5线程池执行器相对于 Java-7 ForkJoinPool 的优势是什么?,觉得答案不够直接。

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

我也从 Javaworld.com中读到了 Google 关于 Java 提示: 何时使用 ForkJoinPool vs ExecutorService的热门话题,但是这篇文章并没有回答标题问题 什么时候,它主要谈论的是 api 的差异..。

50075 次浏览

Fork-join 允许您轻松地执行分而治之的作业,如果您想在 ExecutorService中执行这些作业,则必须手动实现这些作业。在实践中,ExecutorService通常用于并发处理许多独立请求(又名事务) ,当您想要加速一个一致的作业时,可以使用 fork-join。

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

如果您尝试使用传统的线程(例如,通过 ExecutorService)来解决这样的递归问题,那么最终将导致线程被绑定,等待其他线程向它们交付结果。

另一方面,如果问题没有这些特征,那么使用 fork-join 就没有真正的好处。


参考文献:

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

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

  • 工作窃取(空闲线程从有任务的线程窃取工作 排队超过当前处理能力)
  • 能够递归地分解任务并收集结果。 (显然,这个需求必须与 并行处理概念的概念... 但缺乏一个坚实的 实现框架,直至 Java7)

如果并行处理需求是严格递归的,那么别无选择,只能使用 Fork-Join,否则应该使用 Execator 或 Fork-Join 框架,尽管可以说 Fork-Join 更好地利用了资源,因为空闲线程从更繁忙的线程“窃取”了一些任务。

Java8在执行程序中提供了另一个 API

static ExecutorService  newWorkStealingPool()

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

通过添加这个 API,执行者提供了不同类型的 执行服务选项。

根据您的要求,您可以选择其中之一,或者您可以留意 线程池执行器,它提供了更好的控制有限任务队列大小,RejectedExecutionHandler机制。

  1. static ExecutorService newFixedThreadPool(int nThreads)

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

  2. 创建一个线程池,该线程池可以调度命令在给定延迟之后运行,或者定期执行。

  3. 创建一个线程池,该线程池根据需要创建新线程,但在以前构建的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。

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

这些 API 的目标都是满足应用程序各自的业务需求。使用哪一个将取决于您的用例需求。

例如:。

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

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

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

有一个更好的 文章PeterLawrey看看 ExecutorService用例。

相关的 SE 问题:

Java Fork/Join pool、 ExecutorService 和 CountDownLatch

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

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

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

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

而 Execator 服务创建询问的线程数,并应用阻塞队列来存储所有剩余的等待任务。如果使用了 cachedExecterService,它将为每个作业创建单个线程,并且不会有等待队列。

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

使用传统的线程池实现 fork-join 也很有挑战性,因为 fork-join 任务花费大量时间等待其他任务。这种行为会导致线程饥饿死锁,除非仔细选择参数来绑定创建的任务数量,或者池本身是无界的。传统的线程池是为相互独立的任务设计的,而且在设计时考虑了可能阻塞的粗粒度任务ーー fork-join 解决方案两者都不产生。

我推荐阅读整篇文章,因为它有一个很好的例子来说明为什么要使用 fork-join 池。它是在 ForkJoinPool 正式成为正式文件之前编写的,因此他提到的 coInvoke()方法变成了 invokeAll()