并发编程之:ForkJoin
大家好,我是小黑,一个在互联网苟且偷生的农民工。
在JDK1.7中引入了一种新的Fork/Join线程池,它可以将一个大的任务拆分成多个小的任务并行执行并汇总执行结果。
Fork/Join采用的是分而治之的基本思想,分而治之就是将一个复杂的任务,按照规定的阈值划分成多个简单的小任务,然后将这些小任务的结果再进行汇总返回,得到最终的任务。
分治法
分治法是计算机领域常用的算法中的其中一个,主要思想就是将将一个规模为N的问题,分解成K个规模较小的子问题,这些子问题相互独立且与原问题性质相同;求解出子问题的解,合并得到原问题的解。
解决问题的思路
- 分割原问题;
- 求解子问题;
- 合并子问题的解为原问题的解。
使用场景
二分查找,阶乘计算,归并排序,堆排序、快速排序、傅里叶变换都用了分治法的思想。
ForkJoin并行处理框架
在JDK1.7中推出的ForkJoinPool线程池,主要用于ForkJoinTask任务的执行,ForkJoinTask是一个类似线程的实体,但是比普通线程更轻量。
我们来使用ForkJoin框架完成以下1-10亿求和的代码。
public class ForkJoinMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> rootTask = forkJoinPool.submit(new SumForkJoinTask(1L, 10_0000_0000L));
System.out.println("计算结果:" + rootTask.get());
}
}
class SumForkJoinTask extends RecursiveTask<Long> {
private final Long min;
private final Long max;
private Long threshold = 1000L;
public SumForkJoinTask(Long min, Long max) {
this.min = min;
this.max = max;
}
@Override
protected Long compute() {
// 小于阈值时直接计算
if ((max - min) <= threshold) {
long sum = 0;
for (long i = min; i < max; i++) {
sum = sum + i;
}
return sum;
}
// 拆分成小任务
long middle = (max + min) >>> 1;
SumForkJoinTask leftTask = new SumForkJoinTask(min, middle);
leftTask.fork();
SumForkJoinTask rightTask = new SumForkJoinTask(middle, max);
rightTask.fork();
// 汇总结果
return leftTask.join() + rightTask.join();
}
}
上述代码逻辑可通过下图更加直观的理解。

ForkJoin框架实现
在ForkJoin框架中重要的一些接口和类如下图所示。

ForkJoinPool
ForkJoinPool是用于运行ForkJoinTasks的线程池,实现了Executor接口。
可以通过new ForkJoinPool()直接创建ForkJoinPool对象。
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode){
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
通过查看构造方法源码我们可以发现,在创建ForkJoinPool时,有以下4个参数:
- parallelism:期望并发数。默认会使用
Runtime.getRuntime().availableProcessors()的值 - factory:创建
ForkJoin工作线程的工厂,默认为defaultForkJoinWorkerThreadFactory - handler:执行任务时遇到不可恢复的错误时的处理程序,默认为
null - asyncMode:工作线程获取任务使用FIFO模式还是LIFO模式,默认为LIFO
ForkJoinTask
ForkJoinTask是一个对于在ForkJoinPool中运行任务的抽象类定义。
可以通过少量的线程处理大量任务和子任务,ForkJoinTask实现了Future接口。主要通过fork()方法安排异步任务执行,通过join()方法等待任务执行的结果。
想要使用ForkJoinTask通过少量的线程处理大量任务,需要接受一些限制。
- 拆分的任务中避免同步方法或同步代码块;
- 在细分的任务中避免执行阻塞I/O操作,理想情况下基于完全独立于其他正在运行的任务访问的变量;
- 不允许在细分任务中抛出受检异常。
因为ForkJoinTask是抽象类不能被实例化,所以在使用时JDK为我们提供了三种特定类型的ForkJoinTask父类供我们自定义时继承使用。
- RecursiveAction:子任务不返回结果
- RecursiveTask:子任务返回结果
- CountedCompleter:在任务完成执行后会触发执行
ForkJoinWorkerThread
ForkJoinPool中用于执行ForkJoinTask的线程。
ForkJoinPool既然实现了Executor接口,那么它和我们常用的ThreadPoolExecutor之前又有什么差异呢?
如果们使用ThreadPoolExecutor来完成分治法的逻辑,那么每个子任务都需要创建一个线程,当子任务的数量很大的情况下,可能会达到上万个,那么使用ThreadPoolExecutor创建出上万个线程,这显然是不可行、不合理的;
而ForkJoinPool在处理任务时,并不会按照任务开启线程,只会按照指定的期望并行数量创建线程。在每个线程工作时,如果需要继续拆分子任务,则会将当前任务放入ForkJoinWorkerThread的任务队列中,递归处理直到最外层的任务。
工作窃取算法
ForkJoinPool的各个工作线程都会维护一个各自的任务队列,减少线程之间对于任务的竞争;
每个线程都会先保证将自己队列中的任务执行完,当自己的任务执行完之后,会去看其他线程的任务队列中是否有未处理完的任务,如果有则会帮助其他线程执行;
为了减少在帮助其他线程执行任务时发生竞争,会使用双端队列来存放任务,被窃取的任务只会从队列的头部获取任务,而正常处理的线程每次都是从队列的尾部获取任务。

优点
充分利用了线程资源,避免资源的浪费,并且减少了线程间的竞争。
缺点
需要给每个线程开辟一个队列空间;在工作队列中只有一个任务时同样会存在线程竞争。
最后
如果觉得文章对你有点帮助,不妨扫码点个关注。我是小黑,下期见~

并发编程之:ForkJoin的更多相关文章
- 并发编程之 Fork-Join 分而治之框架
前言 "分而治之" 一直是一个有效的处理大量数据的方法.著名的 MapReduce 也是采取了分而治之的思想.简单来说,就是如果你要处理1000个数据,但是你并不具备处理1000个 ...
- [转载]并发编程之Operation Queue和GCD
并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- 并发编程之wait()、notify()
前面的并发编程之volatile中我们用程序模拟了一个场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一个线程t2监听list中的size,当size等于5时,t2线程 ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- python并发编程之Queue线程、进程、协程通信(五)
单线程.多线程之间.进程之间.协程之间很多时候需要协同完成工作,这个时候它们需要进行通讯.或者说为了解耦,普遍采用Queue,生产消费模式. 系列文章 python并发编程之threading线程(一 ...
- python并发编程之gevent协程(四)
协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...
- python并发编程之asyncio协程(三)
协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...
- python并发编程之multiprocessing进程(二)
python的multiprocessing模块是用来创建多进程的,下面对multiprocessing总结一下使用记录. 系列文章 python并发编程之threading线程(一) python并 ...
随机推荐
- 微信小程序测试点
一.测试范围 1.权限测试 需要检查以下几种情况下微信用户访问的权限 1)未授权微信登录小程序 未授权时,一般使用一些业务功能的时候,都会弹出提醒:先授权再操作对应功能.or在提交数据到后台的时候,会 ...
- 06-SpringCloud Eureka
Eureka基础知识 什么是服务治理 Spring Cloud封装了Netflix 公司开发的Eureka模块来实现服务治理 在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理 ...
- 如何保证前端项目上线后的安全?webfunny已总结前端最关键的12大指标
实时监控大屏 众所周知:实时流量大屏,是用来监控前端项目上线质量的. 如大家所知,监控系统会监控线上应用的各项指标,如:错误.白屏.耗时等等,但是仔细一想,即使有这些监控,我们也不一定能够保证线上 ...
- Linux、Windows 下手动生成 sha256 等类型的校验文件
目录 1 - 校验文件的作用 2 - Linux 下生成校验文件 3 - Windows 下生成校验文件 参考资料 版权声明 1 - 校验文件的作用 从网服务器下载文件,尤其是比较大的文件时,很容易由 ...
- mybaits进阶01
在以上mybait入门的改进(增加了接口让增删改查 后期跟容易) 注意:主配置文件和映射配置文件内容不变,但是映射文件要和对应接口放于同目录下并且名称必须相同 一.接口创建 public interf ...
- 题解 block
传送门 如果不想让next_permutation()自动忽略重复元素,可以在比较函数里加个rk之类的东西使它们不同(next_permutation()不用等于号) 关于第一问:貌似也是一个挺常见的 ...
- Mysql生成UUID的SQL语句
大写的UUID: SELECT UPPER(UUID()); 小写的UUID: SELECT LOWER(UUID()); SELECT UUID(); 去掉横杠的UUID: SELECT REPLA ...
- .Net Core 集成 Redis
首先安装RedisServer 安装教程可参照 http://www.redis.cn/download.html 或者 https://www.runoob.com/redis/redis-inst ...
- clickhouse物化视图
今天来简单介绍一下clickhouse的物化视图 物化视图支持表引擎,数据保存形式由它的表引擎决定,创建物化视图的完整语法如下: create materialized view mv_log eng ...
- ES6扩展——箭头函数
1.箭头函数 在es6中,单一参数的单行箭头函数语法结构可以总结如下: const 函数名 = 传入的参数 => 函数返回的内容,因此针对于 const pop = arr => arr. ...