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框架实 ...
随机推荐
- Python download a image (or a file)
http://stackoverflow.com/questions/13137817/how-to-download-image-using-requests import shutil impor ...
- Python基础之文件
输出一行一行的,效率更高 一个任务: 主函数:
- DP系列——树形DP(Codeforces543D-Road Improvement)
一.题目链接 http://codeforces.com/problemset/problem/543/D 二.题意 给一棵树,一开始所有路都是坏的.询问,以每个节点$i$为树根,要求从树根节点到其他 ...
- [maven] 实战笔记 - maven 安装配置
1.下载地址http://maven.apache.org/download.html 2.windows下安装maven(1)下载 apache-maven-3.0-bin.zip 解压到任意目录下 ...
- logger示例
胜哥版 打印日志是很多程序的重要需求,良好的日志输出可以帮我们更方便的检测程序运行状态.Python标准库提供了logging模块,让我们也可以方便的在Python中打印日志. 日志介绍 完整的使用方 ...
- Selenium2+python自动化64-100(大结局)[已出书]
前言 小编曾经说过要写100篇关于selenium的博客文章,前面的64篇已经免费放到博客园供小伙伴们学习,后面的内容就不放出来了,高阶内容直接更新到百度阅读了. 一.百度阅读地址: 1.本书是在线阅 ...
- html链接路径
html链接的相对路径与绝对路径 绝对路径 完整的一个路径就是绝对路径,即包含schema://host[:port#]/path/.../[?query-string][#anchor] 例:htt ...
- VB.Net条形码编程的方法
一.条形码的读取用过键盘口式的扫条码工具的朋友就知道,它就如同在鍵盘上按下数字鍵一样,基本不需任何编程和处理.但如果你使用的是其它接口的话,可能你就要为该设备编写通讯代码了.以下有一段简单的25针串口 ...
- tensorflow 基本函数(1.tf.split, 2.tf.concat,3.tf.squeeze, 4.tf.less_equal, 5.tf.where, 6.tf.gather, 7.tf.cast, 8.tf.expand_dims, 9.tf.argmax, 10.tf.reshape, 11.tf.stack, 12tf.less, 13.tf.boolean_mask
1. tf.split(3, group, input) # 拆分函数 3 表示的是在第三个维度上, group表示拆分的次数, input 表示输入的值 import tensorflow ...
- PO BO VO DTO POJO DAO概念及其作用
J2EE开发中大量的专业缩略语很是让人迷惑,尤其是跟一些高手讨论问题的时候,三分钟就被人家满口的专业术语喷晕了,PO VO BO DTO POJO DAO,一大堆的就来了(听过老罗对这种现象的批判的朋 ...