Java的Fork / Join与ExecutorService-何时使用哪个?

问题描述 投票:52回答:6

我刚读完这篇文章:What's the advantage of a Java-5 ThreadPoolExecutor over a Java-7 ForkJoinPool?,并认为答案不够充分。

您能用简单的语言和示例解释一下,Java 7的Fork-Join框架与较早的解决方案之间的权衡是什么?

[我还从Java Tip: When to use ForkJoinPool vs ExecutorService阅读了Google在主题javaworld.com上的第一名,但文章没有回答标题问题何时,它主要讨论的是api的区别...

java multithreading concurrency executorservice fork-join
6个回答
47
投票

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


32
投票

Fork-join特别适合recursive问题,其中任务涉及运行子任务,然后处理其结果。 (这通常被称为“分而治之”……但是并不能揭示其本质特征。)

[如果您尝试使用常规线程(例如,通过ExecutorService)解决此类递归问题,则最终将线程捆绑在一起,等待其他线程向他们传递结果。

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


参考:


14
投票

Java 8在执行程序中提供了另外一个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)

  4. ] >>

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

  5. [static ExecutorService newWorkStealingPool(int parallelism)

  6. ] >>

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

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

    例如

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

  7. 如果要优化递归任务的大量计算的性能,请使用ForkJoinPoolnewWorkStealingPool

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

  9. [在article用例上,再看PeterLawrey的另一个不错的ExecutorService

    相关的SE问题:

    java Fork/Join pool, ExecutorService and CountDownLatch

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

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

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

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

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

  • [窃取工作(空闲线程从具有任务的线程中窃取工作排队超过当前可以处理的数量)
  • 能够递归分解任务并收集结果的能力。(显然,此要求必须与并行处理概念的概念...但是缺乏扎实的基础Java 7之前的Java实现框架)

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

  • Fork Join是ExecuterService的实现。主要区别在于此实现创建了DEQUE工作池。从一侧插入任务但从任一侧撤回任务的位置。这意味着如果创建了new ForkJoinPool(),它将寻找可用的CPU并创建那么多工作线程。然后,它将负载平均分配给每个线程。但是,如果一个线程运行缓慢而其他线程运行很快,则它们将从慢速线程中选择任务。从背面。以下步骤将更好地说明盗窃行为。

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

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

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

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


    5
    投票

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

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


    4
    投票

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

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


    4
    投票

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

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

    © www.soinside.com 2019 - 2024. All rights reserved.