Fork/Join框架解决什么问题?
Java7中引入Fork/Join框架,为了解决传统线程池ThreadPoolExecutor的两个明显缺点:
- 无法对大任务进行拆分,某些大任务任务只能由单线程执行,影响任务的执行效率。
- 工作线程从队列中获取任务时存在竞争情况。
而Fork/Join框架最适合计算密集型任务,而且最好是非阻塞任务。因为有任务拆分和任务窃取这两个特性:
- 任务切分:Fork/Join的核心类ForkJoinPool,可以将其他线程向它提交的任务,分割成更小粒度的子任务,让更多的线程参与执行;(这些子任务由ForkJoinPool内部的工作线程并行执行。)
- 任务窃取:允许空闲线程从繁忙线程双端队列的尾部拿任务。这样能充分地利用空闲线程,并减少竞争。窃取队列(work-stealing queue)只支持push,pop和poll约束比其他鲜橙多
使用Fork/Join的架构
Fork/Join 计算框架主要包含两部分:一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。
-
ForkJoinPool维护了一个队列数组 WorkQueue(WorkQueue[]),而不像传统执行池 Worker+Queue 的组合,这样在提交任务和线程任务的时候大幅度减少碰撞。
-
ForkJoinPool 内的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。通过ForkJoinPool的invoke() 或submit() 方法,将任务提交到对应的工作队列的top中,而工作线程在处理自己的工作队列时又从top中取任务执行。即用LIFO以保证任务的有序执行。
-
ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。
ForkJoinTask工作原理
提交任务有三种方式:
-
execute类型的方法在提交任务后,不会返回结果。ForkJoinPool不仅允许提交ForkJoinTask类型任务,还允许提交Runnable任务。执行Runnable类型任务时,将会转换为ForkJoinTask类型。由于任务是不可切分的,所以这类任务无法获得任务拆分这方面的效益,不过仍然可以获得任务窃取带来的好处和性能提升。
-
invoke方法接受ForkJoinTask类型的任务,并在任务执行结束后,返回泛型结果。如果提交的任务是null,将抛出空指针异常。
-
submit方法支持三种类型的任务提交:ForkJoinTask类型、Callable类型和Runnable类型。在提交任务后,将返回ForkJoinTask类型的结果。如果提交的任务是null,将抛出空指针异常,并且当任务不能按计划执行的话,将抛出任务拒绝异常。
整个工作流程:
Fork/Join的使用衡量指标
任务总数、单任务执行耗时以及并行数 都会影响到Fork/Join的性能。所以,当你使用Fork/Join框架时,你需要谨慎评估这三个指标,最好能通过模拟对比评估。在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。
为什么工作线程总是从头部获取任务,窃取线程从尾部获取任务?
这样做的主要原因是为了提高性能,工作线程从头部获取最近提交的任务,可以增加资源仍分配在CPU缓存中的机会,这样CPU处理起来要快一些。而窃取者之所以从尾部获取任务,则是为了降低线程之间的竞争可能,而且队列中比较旧的任务更有可能粒度较大没被分割,所以空闲的线程更有精力完成这些粒度较大的任务。