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框架实 ...
随机推荐
- 查询反模式 - GroupBy和HAVING的理解
为了最简单地说明问题,我特地设计了一张这样的表. 一.GROUP BY单值规则 规则1:单值规则,跟在SELECT后面的列表,对于每个分组来说,必须返回且仅仅返回一个值. 典型的表现就是跟在SELEC ...
- java get post 请求
package wzh.Http; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStr ...
- 常见的加密和解密算法—MD5
一.MD5加密概述 Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护.该算法的文件号为RFC 13 ...
- Solr中Facet用法和Group用法
Group分组划分结果,返回的是分组结果: Facet分组统计,侧重统计,返回的是分组后的数量: 一.Group用法: //组查询基础配置params.set(GroupParams.GROUP, & ...
- 好记性不如烂笔头-linux学习笔记5mysql主从复制
mysql主从复制的原理 mysql master服务器,开启bin-log日志,开启IO线程 slave服务器,开启IO线程,开启SQL线程(执行SQL) 1)slave服务器,通过授权用户开启IO ...
- Ubuntu安装Chrome过程中的细节
Ubuntu中的默认浏览器是Firefox,但是一直以来都认为Chrome更加优秀.下面记录一下在Ubuntu下安装Chrome的过程,也回顾一下Ubuntu系统中的一些细节. 大多数Linux安装软 ...
- delphi 路径函数
delphi path functionIncludeTrailingPathDelimiterIncludeTrailingPathDelimiter ensures that a path nam ...
- Git 软件开发过程
一.关于Git与Subversion的区别 二.目前我们用Subversion是怎么执行软件过程的 三.优势与缺点 架构 * Git:分布式,所有的teammates本地可以clone一份独立完整的仓 ...
- C语言结合汇编开发系统内核
- 九度oj-1533 最长上升子序列 (LIS)
http://ac.jobdu.com/problem.php?pid=1533 题目描述: 给定一个整型数组, 求这个数组的最长严格递增子序列的长度. 譬如序列1 2 2 4 3 的最长严格递增子序 ...