Fork/Join 型线程池与 Work-Stealing 算法
JDK 1.7 时,标准类库添加了 ForkJoinPool
,作为对 Fork/Join 型线程池的实现。Fork 在英文中有 分叉 的意思,而 Join 有 合并 的意思。ForkJoinPool
的功能也是如此:Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,然后进行合并,将合并的结果作为大任务的结果 —— 并且这会是一个递归的过程 —— 因为任务如果足够大,可以将任务多级分叉直到任务足够小。
由此可见,ForkJoinPool
可以满足 并行 地实现 分治算法(Divide-and-Conquer) 的需要。
ForkJoinPool
的类图如下:
可以看到 ForkJoinPool
实现了 ExecutorService
接口,所以首先 ForkJoinPool
也是一个 线程池。因而 Runnable
和 Callable
类型的任务,ForkJoinPool
也可以通过 submit
、invokeAll
和 invokeAny
等方法来执行。但是标准类库还为 ForkJoinPool
定义了一种新的任务,它就是 ForkJoinTask<V>
。
ForkJoinTask
相关类图:
ForkJoinTask<V>
用来专门定义 Fork/Join 型任务 —— 完成将大任务分割为小任务以及合并结果的工作。一般我们不需要直接继承 ForkJoinTask<V>
,而是继承它的子类 RecursiveAction
和 RecursiveTask
并实现对应的抽象方法 —— compute
。其中,RecursiveAction
是不带返回值的 Fork/Join 型任务,所以使用此类任务并不产生结果,也就不涉及到结果的合并;而 RecursiveTask
是带返回值的 Fork/Join 型任务,使用此类任务需要我们进行结果的合并。通过 fork
方法,我们可以产生子任务并执行;通过 join
方法,我们可以获得子任务的结果。
ForkJoinPool
用三种方法用来执行 ForkJoinTask
:
invoke
方法:
invoke
方法用来执行一个带返回值的任务(通常继承自RecursiveTask
),并且该方法是阻塞的,直到任务执行完毕,该方法才会停止阻塞并返回任务的执行结果。
submit
方法:
除了从 ExecutorService
继承的 submit
方法外,ForkJoinPool
还定义了用来执行 ForkJoinTask
的 submit
方法 —— 一般该 submit
方法用来执行带返回值的ForkJoinTask
(通常继承自RecursiveTask
)。该方法是非阻塞的,调用之后将任务提交给 ForkJoinPool
去执行便立即返回,返回的便是已经提交到 ForkJoinPool
去执行的 task —— 由类图可知 ForkJoinTask
实现了 Future
接口,所以可以直接通过 task 来和已经提交的任务进行交互。
execute
方法:
除了从 Executor
获得的 execute
方法外,ForkJoinPool
也定义了用来执行ForkJoinTask
的 execute
方法 —— 一般该 execute
方法用来执行不带返回值的ForkJoinTask
(通常继承自RecursiveAction
) ,该方法同样是非阻塞的。
现在让我们来实践下 ForkJoinPool
的功能:计算 π 的值。
计算 π 的值有一个通过多项式方法,即:
π = 4 * (1 - 1/3 + 1/5 - 1/7 + 1/9 - ……)
多项式的项数越多,计算出的 π 的值越精确。
首先我们定义用来估算 π 的 PiEstimateTask
:
static class PiEstimateTask extends RecursiveTask<Double> {
private final long begin;
private final long end;
private final long threshold; // 分割任务的临界值
public PiEstimateTask(long begin, long end, long threshold) {
this.begin = begin;
this.end = end;
this.threshold = threshold;
}
@Override
protected Double compute() {
if (end - begin <= threshold) {
int sign = 1; // 符号,取 1 或者 -1
double result = 0.0;
for (long i = begin; i < end; i++) {
result += sign / (i * 2.0 + 1);
sign = -sign;
}
return result * 4;
}
// 分割任务
long middle = (begin + end) / 2;
PiEstimateTask leftTask = new PiEstimateTask(begin, middle, threshold);
PiEstimateTask rightTask = new PiEstimateTask(middle, end, threshold);
leftTask.fork(); // 异步执行 leftTask
rightTask.fork(); // 异步执行 rightTask
double leftResult = leftTask.join(); // 阻塞,直到 leftTask 执行完毕返回结果
double rightResult = rightTask.join(); // 阻塞,直到 rightTask 执行完毕返回结果
return leftResult + rightResult; // 合并结果
}
}
然后我们使用 ForkJoinPool
的 invoke
执行 PiEstimateTask
:
public class ForkJoinPoolTest {
public static void main(String[] args) throws Exception {
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
// 计算 10 亿项,分割任务的临界值为 1 千万
PiEstimateTask task = new PiEstimateTask(0, 1_000_000_000, 10_000_000);
double pi = forkJoinPool.invoke(task); // 阻塞,直到任务执行完毕返回结果
System.out.println("π 的值:" + pi);
forkJoinPool.shutdown(); // 向线程池发送关闭的指令
}
}
运行结果:
我们也可以使用 submit
方法异步的执行任务(此处 submit
方法返回的 future 指向的对象即提交任务时的 task):
public static void main(String[] args) throws Exception {
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
PiEstimateTask task = new PiEstimateTask(0, 1_000_000_000, 10_000_000);
Future<Double> future = forkJoinPool.submit(task); // 不阻塞
double pi = future.get();
System.out.println("π 的值:" + pi);
System.out.println("future 指向的对象是 task 吗:" + (future == task));
forkJoinPool.shutdown(); // 向线程池发送关闭的指令
}
运行结果:
值得注意的是,选取一个合适的分割任务的临界值,对 ForkJoinPool
执行任务的效率有着至关重要的影响。临界值选取过大,任务分割的不够细,则不能充分利用
CPU;临界值选取过小,则任务分割过多,可能产生过多的子任务,导致过多的线程间的切换和加重 GC
的负担从而影响了效率。所以,需要根据实际的应用场景选择一个合适的分割任务的临界值。
ForkJoinPool
相比于 ThreadPoolExecutor
,还有一个非常重要的特点(优点)在于,ForkJoinPool
具有 Work-Stealing (工作窃取)的能力。所谓 Work-Stealing,在 ForkJoinPool
中的实现为:线程池中每个线程都有一个互不影响的任务队列(双端队列),线程每次都从自己的任务队列的队头中取出一个任务来运行;如果某个线程对应的队列
已空并且处于空闲状态,而其他线程的队列中还有任务需要处理但是该线程处于工作状态,那么空闲的线程可以从其他线程的队列的队尾取一个任务来帮忙运行
—— 感觉就像是空闲的线程去偷人家的任务来运行一样,所以叫 “工作窃取”。
Work-Stealing 的适用场景是不同的任务的耗时相差比较大,即某些任务需要运行较长时间,而某些任务会很快的运行完成,这种情况下用
Work-Stealing 很合适;但是如果任务的耗时很平均,则此时 Work-Stealing
并不适合,因为窃取任务时也是需要抢占锁的,这会造成额外的时间消耗,而且每个线程维护双端队列也会造成更大的内存消耗。所以 ForkJoinPool
并不是 ThreadPoolExecutor
的替代品,而是作为对 ThreadPoolExecutor
的补充。
总结:ForkJoinPool
和 ThreadPoolExecutor
都是 ExecutorService
(线程池),但ForkJoinPool
的独特点在于:
ThreadPoolExecutor
只能执行Runnable
和Callable
任务,而ForkJoinPool
不仅可以执行Runnable
和Callable
任务,还可以执行 Fork/Join 型任务 ——ForkJoinTask
—— 从而满足并行地实现分治算法的需要;ThreadPoolExecutor
中任务的执行顺序是按照其在共享队列中的顺序来执行的,所以后面的任务需要等待前面任务执行完毕后才能执行,而ForkJoinPool
每个线程有自己的任务队列,并在此基础上实现了 Work-Stealing 的功能,使得在某些情况下ForkJoinPool
能更大程度的提高并发效率。
Fork/Join 型线程池与 Work-Stealing 算法的更多相关文章
- JUC组件扩展(二)-JAVA并行框架Fork/Join(四):监控Fork/Join池
Fork/Join 框架是为了解决可以使用 divide 和 conquer 技术,使用 fork() 和 join() 操作把任务分成小块的问题而设计的.主要实现这个行为的是 ForkJoinPoo ...
- fork/join使用示例
fork/join框架是用多线程的方式实现分治法来解决问题.fork指的是将问题不断地缩小规模,join是指根据子问题的计算结果,得出更高层次的结果. fork/join框架的使用有一定的约束条件: ...
- Java并发——Fork/Join框架
为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...
- Java 7 Fork/Join 框架
在 Java7引入的诸多新特性中,Fork/Join 框架无疑是重要的一项.JSR166旨在标准化一个实质上可扩展的框架,以将并行计算的通用工具类组织成一个类似java.util中Collection ...
- 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验
JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...
- Java8新特性 并行流与串行流 Fork Join
并行流就是把一个内容分成多个数据块,并用不同的线程分 别处理每个数据块的流. Java 8 中将并行进行了优化,我们可以很容易的对数据进行并 行操作. Stream API 可以声明性地通过 para ...
- 初步了解Fork/Join框架
框架介绍 Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个子任务,最终汇总每个子任务的执行结果以得到大任务结果的框架.Fork/Join框架要完成两件事 ...
- Java并发——Fork/Join框架与ForkJoinPool
为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...
- Fork/Join 框架-设计与实现(翻译自论文《A Java Fork/Join Framework》原作者 Doug Lea)
作者简介 Dong Lea任职于纽约州立大学奥斯威戈分校(State University of New York at Oswego),他发布了第一个广泛使用的java collections框架实 ...
随机推荐
- 7z 7zip 日期、时间,文件名
from: http://hi.baidu.com/guicomeon/item/c0957c373972fbc52f8ec26e 先说明一点,要注意区分当前所使用的系统,中文系统和英文系统是有区别的 ...
- "废物利用"也抄袭——“完全”DIY"绘图仪"<二、下位机程序设计>
就不说怎么组装了吧,一把辛酸泪.说程序,因为这有两把辛酸泪……一把给下位机的C代码一把为了VB.NET的图像处理……不过就上上一篇说的,它们可以正确运行了,并且今天克服了Arduino上电过程中步进电 ...
- 关于 android 环信无法正确获取昵称的问题
本案例中 username 记录成 userId了, nick 始终为空...,这是 getNick() 取得的就是 username..... 如果想自己取得自己系统的nickname则 做以下调整 ...
- appium的三种等待方式 (还没实践过,记录在此)
参考:https://testerhome.com/topics/2576
- 第5课 Qt Creator工程介绍
1. QT Creator工程管理(一个工程包含不同类型的文件) (1).pro项目文件 (2).pro.user用户配置描述文件 (3).h头文件 (4).cpp源文件 (5).ui界面描述文件 ( ...
- ES6系列_5之字符串模版
1.字符串模板对比引入: (1).之前我们也可以使用JavaScript输出模版字符串,通常是下面这样的: var restult= "姓名: <b>"+person. ...
- Rhythmk 学习 Hibernate 06 - Hibernate 表间关系 [One To One]
1.One To One 单相 背景: 古代一个老婆 只能关联一个老公 husband.java package com.rhythmk.model; public class husband { ...
- 可视化库-Matplotlib-条形图(第四天)
1.画两个条形图,bar和barh, 同时axes[0].axhline画一条横线,axes[1].axvline画一条竖线 import numpy as np import matplotlib. ...
- redis一主二从三哨兵
redis做集群的时候有很多种配置方法,一主二从三哨兵这种模式是官网推荐的.,写配置文件链接的时候,写的是哨兵地址,不是IP,用户名,密码之类的. 一主二从很好理解,一个主的redis,实时备份到两个 ...
- python模块的打包
python模块的打包方法: http://blog.csdn.net/five3/article/details/7847551