Java的Fork / Join与ExecutorService-什么时候使用?


69

我刚读完这篇文章:与Java-7 ForkJoinPool相比,Java-5 ThreadPoolExecutor有什么优势?并认为答案不够直接。

您能用简单的语言和示例来说明Java 7的Fork-Join框架与较旧的解决方案之间的权衡吗?

我还阅读了关于Java技巧的Google排名第一的提示:何时使用javaworld.com上的ForkJoinPool与ExecutorService,但是本文没有回答when的标题问题,它主要讨论的是api的区别...

Answers:


59

通过Fork-join,您可以轻松执行分割和征服作业,如果要在中执行,则必须手动实施ExecutorService。实际上ExecutorService,通常用于同时处理许多独立的请求(又称为事务),并且当您要加速一项连贯的工作时,可以使用fork-join。


6
+1 Fork-Join解决了特定类型的问题。如果您没有此类问题,请使用ExecutorService,因为无论如何,这都是Fork-Join所使用的。
彼得·劳瑞

@JakubK好。如果我要处理1000x1000的图片怎么办。我可以预先分配它或以D&C方式分配它。然后怎样呢?我也想的比较-许多任务VS少,长期生活VS短,大小相等的问题,相较于未等
Parobay

如果您可以分别处理所有零件然后合并结果,则应使用fork-join
Jakub Kubrynski 2014年

1
ForkJoinPool还针对D&C任务进行了优化,其“工作窃取”算法可在内部处理子任务,以最大限度地提高CPU利用率
mwdev

39

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

如果您尝试使用常规线程(例如,通过ExecutorService)来解决此类递归问题,那么最终将束缚线程,等待其他线程向其传递结果。

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


参考文献:


无论如何,分而治之与递归有所不同。甚至Fork联接池也可能不得不等待找到递归运算(如斐波那契数列)的结果,不是吗?
jayendra bhatt

1)分而治之是递归的一种形式。阅读en.wikipedia.org/wiki/Divide-and-conquer_algorithm的第一句话。2)否。任务要么自行完成工作,要么提交子任务。在后一种情况下,它不等待子任务完成。在这里看到的例子:docs.oracle.com/javase/tutorial/essential/concurrency/...
斯蒂芬ç

20

Java 8在执行程序中提供了另一种API

static ExecutorService  newWorkStealingPool()

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

通过添加此API,Executors提供了不同类型的ExecutorService选项。

根据您的需求,您可以选择其中之一,也可以寻找ThreadPoolExecutor,它可以更好地控制Bounded Task Queue Size(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


6

布莱恩·格茨(Brian Goetz)最好地描述了这种情况:https : //www.ibm.com/developerworks/library/j-jtp11137/index.html

使用常规线程池来实现fork-join也是一个挑战,因为fork-join任务的大部分生命都在等待其他任务。此行为是导致线程饥饿死锁的秘诀,除非精心选择参数以限制创建的任务数或池本身不受限制。常规线程池是为彼此独立的任务而设计的,并且在设计时还考虑了潜在的阻塞,粗粒度的任务-fork-join解决方案不会产生任何结果。

我建议阅读全文,因为它有一个很好的示例说明为什么要使用fork-join池。它是在ForkJoinPool正式发布之前编写的,因此coInvoke()他所指的方法成为invokeAll()


5

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

Fork-Join框架有两个主要特征

  • 工作窃取(空闲线程从任务排队超过当前处理能力的线程中窃取工作)
  • 递归分解任务并收集结果的能力。(显然,此要求必须与并行处理概念一起出现了……但是直到Java 7才在Java中缺少可靠的实现框架)

如果并行处理需求是严格递归的,则别无选择,只能选择Fork-Join,否​​则执行者或Fork-Join框架都应该这样做,尽管可以说Fork-Join由于空闲线程而更好地利用了资源。从繁忙的线程“窃取”某些任务。


4

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,

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.