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框架实 ...
随机推荐
- 2.运行成功的Demo(Python+Appium)
1.打开Appium运行 2.在Pycharm输入代码如下所示: from appium import webdriver desired_caps = {} #初始化 desired_caps['p ...
- Phonegap 自定义插件
一.PhoneGap中js与Java之间相互调用分为同步和异步两种方式 1.同步:js调用Java类的方法,然后Java类的方法直接返回一个值给js端 2.异步:js调用Java类的方法,Java类的 ...
- maven库 mvn依赖
http://maven.outofmemory.cn/ http://mvnrepository.com/ 先执行 mvn clean 然后执行 mvn 命令 如:mvn compile . ...
- [C++ Primer] 第6章: 函数
参数传递 const形参和实参: 顶层const作用于对象本身, 和其他初始化过程一样, 当用实参初始化形参时会忽略掉顶层const, 换句话说, 形参顶层const被忽略掉了, 当形参有顶层cons ...
- SpringCloud初体验:三、Feign 服务间调用(FeignClient)、负载均衡(Ribbon)、容错/降级处理(Hystrix)
FeignOpenFeign Feign是一种声明式.模板化的HTTP客户端. 看了解释过后,可以理解为他是一种 客户端 配置实现的策略,它实现 服务间调用(FeignClient).负载均衡(Rib ...
- 清理Visual Studio中VC++工程里不需要的文件
Visual Studio开发C++,工程的空间几M,几十M甚至几百M的长,生成的中间文件看的眼花缭乱,占空间不说,特别是备份拷贝代码时无奈的等待,有了这个脚本,好吧,整个世界清静了. @echo o ...
- SSH: sshd dead but subsys locked
问题: 查看SSH的状态时,提示错误如下: /etc/init.d/sshd status error: sshd dead but subsys locked 解决方法: sshd -d rm -r ...
- nginx收到空包问题
tcpdump有收包,但是nginx的access.log显示post数据为空 可以通过tcpdump监控端口 http://www.cnblogs.com/linn/p/4792468.html 修 ...
- uwsgi的python2+3多版本共存实操使用virtualenv
1首先,机器需要有python2和python3的可执行环境.确保pip和pip3命令可用.原理就是在哪个环境下安装uwsgi.uwsgi启动的时候,就用的哪个python版本 2安装virtuale ...
- UNITY Destroy()和DestroyImadiate()的区别
using System.Collections; using System.Collections.Generic; using System.Timers; using UnityEngine; ...