原文链接

当我们需要执行大量的小任务时,有经验的Java开发人员都会采用线程池来高效执行这些小任务。然而,有一种任务,例如,对超过1000万个元素的数组进行排序,这种任务本身可以并发执行,但如何拆解成小任务需要在任务执行的过程中动态拆分。这样,大任务可以拆成小任务,小任务还可以继续拆成更小的任务,最后把任务的结果汇总合并,得到最终结果,这种模型就是Fork/Join模型。

Java7引入了Fork/Join框架,我们通过RecursiveTask这个类就可以方便地实现Fork/Join模式。

例如,对一个大数组进行并行求和的RecursiveTask,就可以这样编写:

class SumTask extends RecursiveTask<Long> {

    static final int THRESHOLD = 100;
long[] array;
int start;
int end; SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
} @Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(String.format("compute %d~%d = %d", start, end, sum));
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}

 

编写这个Fork/Join任务的关键在于,在执行任务的compute()方法内部,先判断任务是不是足够小,如果足够小,就直接计算并返回结果(注意模拟了1秒延时),否则,把自身任务一拆为二,分别计算两个子任务,再返回两个子任务的结果之和。

最后写一个main()方法测试:

public static void main(String[] args) throws Exception {
// 创建随机数组成的数组:
long[] array = new long[400];
fillRandom(array);
// fork/join task:
ForkJoinPool fjp = new ForkJoinPool(4); // 最大并发数4
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = fjp.invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}

  

关键代码是fjp.invoke(task)来提交一个Fork/Join任务并发执行,然后获得异步执行的结果。

我们设置任务的最小阀值是100,当提交一个400大小的任务时,在4核CPU上执行,会一分为二,再二分为四,每个最小子任务的执行时间是1秒,由于是并发4个子任务执行,整个任务最终执行时间大约为1秒。

新手在编写Fork/Join任务时,往往用搜索引擎搜到一个例子,然后就照着例子写出了下面的代码:

protected Long compute() {
if (任务足够小?) {
return computeDirect();
}
// 任务太大,一分为二:
SumTask subtask1 = new SumTask(...);
SumTask subtask2 = new SumTask(...);
// 分别对子任务调用fork():
subtask1.fork();
subtask2.fork();
// 合并结果:
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
return subresult1 + subresult2;
}

  

很遗憾,这种写法是错!误!的!这样写没有正确理解Fork/Join模型的任务执行逻辑。

JDK用来执行Fork/Join任务的工作线程池大小等于CPU核心数。在一个4核CPU上,最多可以同时执行4个子任务。对400个元素的数组求和,执行时间应该为1秒。但是,换成上面的代码,执行时间却是两秒。

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

打个比方,假设一个酒店有400个房间,一共有4名清洁工,每个工人每天可以打扫100个房间,这样,4个工人满负荷工作时,400个房间全部打扫完正好需要1天。

Fork/Join的工作模式就像这样:首先,工人甲被分配了400个房间的任务,他一看任务太多了自己一个人不行,所以先把400个房间拆成两个200,然后叫来乙,把其中一个200分给乙。

紧接着,甲和乙再发现200也是个大任务,于是甲继续把200分成两个100,并把其中一个100分给丙,类似的,乙会把其中一个100分给丁,这样,最终4个人每人分到100个房间,并发执行正好是1天。

如果换一种写法:

// 分别对子任务调用fork():
subtask1.fork();
subtask2.fork();

  

这个任务就分!错!了!

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

其实,我们查看JDK的invokeAll()方法的源码就可以发现,invokeAll的N个任务中,其中N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行,这样,就充分利用了线程池,保证没有空闲的不干活的线程。

 

【java并发系列】Fork/Join任务(转)的更多相关文章

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

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

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

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

  3. 【fork/join】java并发编程-fork/join示例

    package com.chinamobile.epic.tako.common.graphite.query.sync.impl; import com.google.common.collect. ...

  4. Java 并发系列之十:java 并发框架(2个)

    1. Fork/Join框架 2. Executor框架 3. ThreadPoolExecutor 4. ScheduledThreadPoolExecutor 5. FutureTask 6. t ...

  5. java 中的fork join框架

    文章目录 ForkJoinPool ForkJoinWorkerThread ForkJoinTask 在ForkJoinPool中提交Task java 中的fork join框架 fork joi ...

  6. Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, Cyc ...

  7. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  8. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  9. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  10. Java 并发系列之二:java 并发机制的底层实现原理

    1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...

随机推荐

  1. Java和Flex整合报错(五)

    1.错误描述 usage: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -help ...

  2. 下载安装eclipse

    其实在配置好jdk之后,只要下载eclipse压缩包下载完解压就行了.很多人在eclipse官网上下载的安装包版本的,下载完了之后又不知道怎么安装.下面我给大家介绍以下如何下载eclipse的Oxyg ...

  3. Java开发速度神器Lombok,Eclipse端安装使用教程

    一.Lombok简介 Lombok是一个代码生成器,可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,通过使用对应的注解,可以在编译源码的时候生成对应的方法. 使用 ...

  4. sql数据库设置自定义消息

    第一步 EXEC sp_addmessage   @msgnum =   50005 ,   @severity =   10 ,   @msgtext =   '更新失败'        ,  @l ...

  5. CentOS 7.x 防火墙开放端口相关用法记录

    前言 防火墙对服务器起到一定的保护作用,所以了解一些相关的操作是很有必要的. 在CentOS 7.x中,有了一种新的防火墙策略,FireWall , 还记得在6.x中用的还是iptables. 这几天 ...

  6. 【洛谷1640】[SCOI2010]连续攻击游戏

    题目描述 lxhgww最近迷上了一款游戏,在游戏里,他拥有很多的装备,每种装备都有2个属性,这些属性的值用[1,10000]之间的数表示.当他使用某种装备时,他只能使用该装备的某一个属性.并且每种装备 ...

  7. 杜教筛:Bzoj3944: sum

    题意 求\(\sum_{i=1}^{n}\varphi(i)和\sum_{i=1}^{n}\mu(i)\) \(n <= 2^{31}-1\) 不会做啊... 只会线性筛,显然不能线性筛 这个时 ...

  8. eclipse 精确查询

    ---恢复内容开始--- ctrl+H(一般都是这个,如果无效看你的自定义快捷键) 输入\b 查询的字符串 \b   后面的正则表达式选框必须选

  9. .net remoting在wpf中的应用

    我做一个remotting的通讯测试,让控制台程序和wpf窗体通讯.具体实现的功能如下: 1.wpf获取信息在控制台上显示 2.控制台启动wpf,以及在屏幕前端显示 首先,我们来看项目结构: 共三个项 ...

  10. onmouse事件

    常用的鼠标事件:onmouseenter,onmouseleave,onmouseover,onmouseout,onmouseup,onmousedown,onmousewheel,onmousem ...