一、介绍

使用 java8 lambda 表达式大半年了,一直都知道底层使用的是 Fork/Join 框架,今天终于有机会来学学 Fork/Join 框架了。

Fork/Join 框架是 Java 7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join 的运行流程示意图:

比如,一个 1+2+3+...+100 的工作任务,我们可以把它 Fork 成 10 个子任务,分别计算这 10 个子任务的运行结果。最后再把 10 个子任务的结果 Join 起来,汇总成最后的结果。

为了减少线程间的竞争,通常把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其它线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。线程的这种执行方式,我们称之为“工作窃取”算法。

二、设计

实现 Fork/Join 框架的设计,大抵需要两步:

1. 分割任务

首先我们需要创建一个 ForkJoin 任务,把大任务分割成子任务,如果子任务不够小,则继续往下分,直到分割出的子任务足够小。

在 Java 中我们可以使用 ForkJoinTask 类,它提供在任务中执行 fork() 和 join() 操作的机制,通常情况下,我们只需要继承它的子类:

  • RecursiveAction — 用于没有返回结果的任务
  • RecursiveTask — 用于有返回结果的任务

2. 任务执行并返回结果

分割的子任务分别放在双端队列里,然后启动几个线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

在 Java 中任务的执行需要通过 ForkJoinPool 来执行。

三、示例

来一个阿里面试题:百万级 Integer 数据量的一个 array 求和。

public class ArrayCountTask extends RecursiveTask<Long> {
/**
* 阈值
*/
private static final Integer THRESHOLD = 10000; private Integer[] array;
private Integer start;
private Integer end; public ArrayCountTask(Integer[] array, Integer start, Integer end) {
this.array = array;
this.start = start;
this.end = end;
} @Override
protected Long compute() {
long sum = 0;
// 最小子任务计算
if (end - start <= THRESHOLD) {
for (int i = start; i < end; i++) {
sum += array[i];
}
} else {
// 把大于阈值的任务继续往下拆分,有点类似递归的思维。 recursive 就是递归的意思。
int middle = (start + end) >>> 1;
ArrayCountTask leftArrayCountTask = new ArrayCountTask(array, start, middle);
ArrayCountTask rightArrayCountTask = new ArrayCountTask(array, middle, end);
// 执行子任务
//leftArrayCountTask.fork();
//rightArrayCountTask.fork(); // invokeAll 方法使用
invokeAll(leftArrayCountTask, rightArrayCountTask); //等待子任务执行完,并得到其结果
Long leftJoin = leftArrayCountTask.join();
Long rightJoin = rightArrayCountTask.join();
// 合并子任务的结果
sum = leftJoin + rightJoin;
} return sum;
}
}
    public static void main(String[] args) {
// 1. 造一个 int 类型的百万级别数组
Integer[] array = new Integer[150000000];
for (int i = 0; i < array.length; i++) {
array[i] = new Random().nextInt(100);
}
// 2.普通方式计算结果
long start = System.currentTimeMillis();
long sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
long end = System.currentTimeMillis();
System.out.println("普通方式计算结果:" + sum + ",耗时:" + (end - start));
long start2 = System.currentTimeMillis();
// 3.fork/join 框架方式计算结果
ArrayCountTask arrayCountTask = new ArrayCountTask(array, 0, array.length);
ForkJoinPool forkJoinPool = new ForkJoinPool();
sum = forkJoinPool.invoke(arrayCountTask);
long end2 = System.currentTimeMillis();
System.out.println("fork/join 框架方式计算结果:" + sum + ",耗时:" + (end2 - start2)); // 结论:
// 1. 电脑 i5-4300m,双核四线程
// 2. 数组量少的时候,fork/join 框架要进行线程创建/切换的操作,性能不明显。
// 3. 数组量超过 100000000,fork/join 框架的性能才开始体现。 }

ForkJoinTask 与一般任务的主要区别在于它需要实现 compute 方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用 fork 方法时,又会进入 compute 方法,看看当前子任务是否需要继续分割成子任务,如果不需要继续分割,则执行当前子任务并返回结果。使用 join 方法会等待子任务执行完并得到其结果。

在执行子任务时调用 fork 方法并不是最佳的选择,最佳的选择是 invokeAll 方法。因为执行 compute() 方法的线程本身也是一个 worker 线程,当对两个子任务调用 fork() 时,这个worker 线程就会把任务分配给另外两个 worker,但是它自己却停下来等待不干活了!这样就白白浪费了 Fork/Join 线程池中的一个 worker 线程,导致了4个子任务至少需要7个线程才能并发执行。

比如甲把 400 分成两个 200 后,fork() 写法相当于甲把一个 200 分给乙,把另一个 200 分给丙,然后,甲成了监工,不干活,等乙和丙干完了他直接汇报工作。乙和丙在把 200 分拆成两个 100 的过程中,他俩又成了监工,这样,本来只需要 4 个工人的活,现在需要 7 个工人才能完成,其中有3个是不干活的。

 

ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成。ForkJoinTask 数组负责将存放程序提交给 ForkJoinPool 的任务;而 ForkJoinWorkerThread 数组负责执行这些任务,ForkJoinWorkerThread 体现的就是“工作窃取”算法。

  • 当我们调用 ForkJoinTask 的 fork 方法时,程序会调用 ForkJoinWorkerThread 的 pushTask 方法异步地执行这个任务,然后立即返回结果。
  • 当我们调用 ForkJoinTask 的 join 方法时,程序会阻塞当前线程并等待获取结果。

ForkJoinPool 使用 submit 或 invoke 提交的区别:invoke 同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit 是异步执行,只有在 Future 调用 get 的时候会阻塞。

ForkJoinPool 继承自 AbstractExecutorService, 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。

多线程编程学习七( Fork/Join 框架).的更多相关文章

  1. Java并发编程(07):Fork/Join框架机制详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.Fork/Join框架 Java提供Fork/Join框架用于并行执行任务,核心的思想就是将一个大任务切分成多个小任务,然后汇总每个小任务 ...

  2. 多线程(五) Fork/Join框架介绍及实例讲解

    什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过For ...

  3. ☕【Java技术指南】「并发编程专题」Fork/Join框架基本使用和原理探究(基础篇)

    前提概述 Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行. 我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一 ...

  4. 并发编程学习笔记(12)----Fork/Join框架

    1. Fork/Join 的概念 Fork指的是将系统进程分成多个执行分支(线程),Join即是等待,当fork()方法创建了多个线程之后,需要等待这些分支执行完毕之后,才能得到最终的结果,因此joi ...

  5. java多线程8:阻塞队列与Fork/Join框架

    队列(Queue),是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的. BlockingQueue 而阻塞队列BlockingQueue除了继承 ...

  6. Java 并发编程 -- Fork/Join 框架

    概述 Fork/Join 框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架.下图是网上流传的 Fork Join 的 ...

  7. Java并发编程--Fork/Join框架使用

    上篇博客我们介绍了通过CyclicBarrier使线程同步,可是上述方法存在一个问题,那就是假设一个大任务跑了2个线程去完毕.假设线程2耗时比线程1多2倍.线程1完毕后必须等待线程2完毕.等待的过程线 ...

  8. Fork/Join 框架-设计与实现(翻译自论文《A Java Fork/Join Framework》原作者 Doug Lea)

    作者简介 Dong Lea任职于纽约州立大学奥斯威戈分校(State University of New York at Oswego),他发布了第一个广泛使用的java collections框架实 ...

  9. Java 7 Fork/Join 框架

    在 Java7引入的诸多新特性中,Fork/Join 框架无疑是重要的一项.JSR166旨在标准化一个实质上可扩展的框架,以将并行计算的通用工具类组织成一个类似java.util中Collection ...

随机推荐

  1. 【CuteJavaScript】Angular6入门项目(1.构建项目和创建路由)

    本文目录 一.项目起步 二.编写路由组件 三.编写页面组件 1.编写单一组件 2.模拟数据 3.编写主从组件 四.编写服务 1.为什么需要服务 2.编写服务 五.引入RxJS 1.关于RxJS 2.引 ...

  2. Docker私有仓库搭建与界面化管理

    一.关于Registry 官方的Docker hub是一个用于管理公共镜像的好地方,我们可以在上面找到我们想要的镜像,也可以把我们自己的镜像推送上去. 但是有时候我们的使用场景需要我们拥有一个私有的镜 ...

  3. OPPO R11刷机初体验

    刷机目的 最初打算是用旧手机搭一个服务器,首先想到的是刷一个Linux系统,但这太难了,我搞不定,然后就想着可以用一些软件比如KSWEB之类的来代替,但是想要访问80端口的话还是需要root,但是普通 ...

  4. Linux下监控用户操作轨迹

    在实际工作当中,都会碰到误删除.误修改配置文件等事件.如果没有堡垒机,要在linux系统上查看到底谁对配置文件做了误操作,特别是遇到删库跑路的事件,当然可以通过history来查看历史命令记录,但如果 ...

  5. Nginx代理缓存功能

    Nginx代理缓存功能      Nginx缓存主要是用于减轻后端服务器的负载,提高网站并发量,提升用户体验度. 注意:Nginx反向代理的缓存功能是由ngx_http_proxy_module提供, ...

  6. JVM CPU Profiler技术原理及源码深度解析

    研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈.Profiling技术是一种在应用运行时收集程序相关信息的动态分析手段,常用的JVM Profiler可以从多个方面对程 ...

  7. ABAP基础语法

    1.数据类型及属性 类型 说明 C N 0到9之间字符组成的数字字符串 D 日期格式必须为 YYYYMMDD T 格式为 24-hour的 HHMMSS I -2.147.483.648 to +2. ...

  8. 域控权限提升PTH攻击

    0x01 漏洞利用条件 1.被pth攻击的计算机未打补丁(KB2871997)2.拿到一台域成员主机并且拿到管理员组的域用户的NTML 3.对方主机存在相同账号并且是管理员组成员 0x02 本地用户N ...

  9. 升级sharepoint2013遇到的坑

    现在要将sharepoint2010,ProjectServer2010升级到2016的版本,需要先升级到2013的版本. 按照官方文档,瞎搞将sharepoint2010升级到2013的版本,中间出 ...

  10. 平时常用sql

    总结一下平时用到最多的sql语句 1.特殊日期 --今天凌晨 ,) --明天凌晨 ,,) --当周周一(每周从周日开始) ,) --当月的第一天 ,) --当月的最后一天 ,,,)) --今年的第一天 ...