并发编程之: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并 ...
随机推荐
- 基于 Clusternet 与 OCM 打造新一代开放的多集群管理平台
背景 随着 5G.物联网设备的爆炸性增长以及智能终端不断增强的计算能力,带来了前所未有的数据量,传统的中心集中式计算捉襟见肘."新基建"战略的实施,工业互联网.车联网/自动驾驶.智 ...
- "百度杯"CTF比赛 十月场——EXEC
"百度杯"CTF比赛 十月场--EXEC 进入网站页面 查看源码 发现了vim,可能是vim泄露,于是在url地址输入了http://21b854b211034489a4ee1cb ...
- JQuery常用属性操作,动画,事件绑定
jQuery 的属性操作 html() 它可以设置和获取起始标签和结束标签中的内容. 跟 dom 属性 innerHTML 一样. text() 它可以设置和获取起始标签和 ...
- linux copy_id
ssh-keygen 产生公钥与私钥对. ssh-copy-id 将本机的公钥复制到远程机器的authorized_keys文件中,ssh-copy-id也能让你有到远程机器的home, ~./ssh ...
- Mantis安装过程笔记
安装平台:Windows Server 2003 R2 Enterprise x64 Edition 软件: EasyPHP-5.3.6.1 mantisbt-1.2.6 安装过程: 首先安装Easy ...
- 刚学spark
https://blog.csdn.net/u013019431/article/details/80776662 在jupyter notebook import pysparkhttps:// ...
- Java:学习什么是多线程
线程是什么 进程是对CPU的抽象,而线程更细化了进程的运行流程 先看一下这个图 线程和进程的关系有 进程中就是线程在执行,所有(主)线程执行完了进程也就结束了 多个线程从1秒钟是同时运行完成,从1纳秒 ...
- 在Activity和附贴的Fragment中同时使用多Surface错乱解决
SurfaceView因为独特的双缓冲机制,在android应用中十分普遍,视频播放器.摄像机预览等都会用到,如果在两个Fragment或者一个Fragment和Activity同时使用都会造成无法正 ...
- 4、二进制安装K8s 之 部署kube-controller-manager
二进制安装K8s 之 部署kube-controller-manager 1.创建配置文件 cat > /data/k8s/config/kube-controller-manager.conf ...
- 【spring 注解驱动开发】spring事务处理原理
尚学堂spring 注解驱动开发学习笔记之 - 事务处理 事务处理 1.事务处理实现 实现步骤: * 声明式事务: * * 环境搭建: * 1.导入相关依赖 * 数据源.数据库驱动.Spring-jd ...