Java-5 ThreadPoolExecutor 比 Java-7 ForkJoinPool 有什么优势?

Java 5以Executor框架的形式引入了对线程池异步任务执行的支持,其核心是由java.util.concurrent.ThreadPoolExecutor实现的线程池。Java 7 以 java.util.concurrent.ForkJoinPool 的形式添加了一个替代线程池。

看看它们各自的API,ForkJoinPool在标准场景中提供了ThreadPoolExecutor功能的超集(尽管严格来说,ThreadPoolExecutor提供了比ForkJoinPool更多的调优机会)。除此之外,分叉/连接任务似乎更快(可能是由于工作窃取调度程序),绝对需要更少的线程(由于非阻塞连接操作),人们可能会得到ThreadPoolExecutor已被ForkJoinPool取代的印象。

但这真的是正确的吗?我读过的所有材料似乎都总结了两种类型的线程池之间相当模糊的区别:

  • ForkJoinPool适用于许多依赖的,任务生成的,短的,几乎从不阻塞(即计算密集型)的任务
  • ThreadPoolExecutor适用于少数,独立,外部生成的,很长的,有时阻塞的任务

这种区别是否正确?我们能就此说些更具体的话吗?


答案 1

ThreadPool (TP) 和 ForkJoinPool (FJ) 针对不同的用例。主要区别在于不同执行程序使用的队列数量,这些队列决定了哪种类型的问题更适合任何一个执行程序。

FJ 执行器具有 n 个(也称为并行度级别)单独的并发队列(deques),而 TP 执行器只有一个并发队列(这些队列/执行器可能是不遵循 JDK 集合 API 的自定义实现)。因此,在生成大量(通常运行时间相对较短)任务的情况下,FJ 执行器的性能会更好,因为独立队列将最大限度地减少并发操作,而不频繁的窃取将有助于负载平衡。在TP中,由于是单一队列,每次下排队都会有并发操作,它会作为相对瓶颈并限制性能。

相反,如果长时间运行的任务相对较少,则 TP 中的单个队列不再是性能瓶颈。但是,与n无关的队列和相对频繁的工作窃取尝试现在将成为FJ中的瓶颈,因为可能会有许多徒劳的尝试来窃取工作,从而增加开销。

此外,FJ 中的工作窃取算法假设从 deque 中窃取的(较旧的)任务将产生足够的并行任务来减少窃取次数。例如,在快速排序或合并排序中,较旧的任务等同于较大的数组,这些任务将生成更多任务并保持队列不为空并减少整体窃取的数量。如果在给定的应用程序中不是这种情况,那么频繁的窃取尝试再次成为瓶颈。这在 ForkJoinPool 的 javadoc 中也有说明:

此类提供状态检查方法(例如 getStealCount()),这些方法旨在帮助开发、调整和监视分叉/联接应用程序。


答案 2

推荐阅读 http://gee.cs.oswego.edu/dl/jsr166/dist/docs/ ForkJoinPool的文档:

ForkJoinPool与其他类型的ExecutorService不同,主要是因为采用了工作窃取:池中的所有线程都试图查找和执行提交到池和/或由其他活动任务创建的任务(如果不存在,最终阻止等待工作)。当大多数任务生成其他子任务(与大多数 ForkJoinTasks 一样)以及当许多小任务从外部客户端提交到池时,这可以实现高效处理。特别是在构造函数中将异步模式设置为 true 时,ForkJoinPools 也可能适用于从未加入的事件样式任务。

分叉连接框架对于并行执行很有用,而执行器服务允许并发执行,并且存在差异。看到这个这个

分叉连接框架还允许工作窃取(使用Deque)。

这篇文章很好读。


推荐