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

百牛信息技术bainiu.ltd整理发布于博客园

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 7 Fork/Join 框架

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

  2. Java的Fork/Join任务

    当我们需要执行大量的小任务时,有经验的Java开发人员都会采用线程池来高效执行这些小任务.然而,有一种任务,例如,对超过1000万个元素的数组进行排序,这种任务本身可以并发执行,但如何拆解成小任务需要 ...

  3. Java Concurrency - Fork/Join Framework

    Normally, when you implement a simple, concurrent Java application, you implement some Runnable obje ...

  4. Java 7 Fork/Join 并行计算框架概览

    应用程序并行计算遇到的问题 当硬件处理能力不能按摩尔定律垂直发展的时候,选择了水平发展.多核处理器已广泛应用,未来处理器的核心数将进一步发布,甚至达到上百上千的数量.而现在 很多的应用程序在运行在多核 ...

  5. Java并发——Fork/Join框架

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...

  6. Java并发——Fork/Join框架与ForkJoinPool

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...

  7. Java使用Fork/Join框架来并行执行任务

    现代的计算机已经向多CPU方向发展,即使是普通的PC,甚至现在的智能手机.多核处理器已被广泛应用.在未来,处理器的核心数将会发展的越来越多. 虽然硬件上的多核CPU已经十分成熟,但是很多应用程序并未这 ...

  8. Java通过Fork/Join来优化并行计算

    Java代码: package Threads; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Recur ...

  9. 我的Java开发学习之旅------>Java使用Fork/Join框架来并行执行任务

    现代的计算机已经向多CPU方向发展,即使是普通的PC,甚至现在的智能手机.多核处理器已被广泛应用.在未来,处理器的核心数将会发展的越来越多. 虽然硬件上的多核CPU已经十分成熟,但是很多应用程序并未这 ...

随机推荐

  1. python(27)- 面向对象练习Ⅰ

    一:定义如下类,并最大程度地重用代码(继承,派生:子类重用父类方法,组合) 老师类 学生类 分数类 课程类 生日类 class People: def __init__(self,name,age,b ...

  2. Django-models-m2m

    在Django的orm中,如果有多对多的字段,那么他的第三张表时自己生成的,参考官方文档发现第三张表可以自己写↓: 而且第三张表好像是可以自定制的 from django.db import mode ...

  3. ZOJ 3156 Taxi (二分匹配+二分查找)

    题目链接:Taxi Taxi Time Limit: 1 Second      Memory Limit: 32768 KB As we all know, it often rains sudde ...

  4. Angularv4入门篇1

    国庆时按照官网的tutorial写了遍官方示例,一知半解,不明白angular的服务的服务为何要单独抽离出来.angular应用是如何启用的.近期打算看下angular的文档部分,然后梳理遍以理解an ...

  5. Android与WebView的插件管理机制

    上一篇文章说到,当利用WebViewClient或者WebChromeClient来处理由html页面传过来的请求的时候,都会将相应的服务名称,操作方法和相应的參数数据传给一个叫PluginManag ...

  6. 关于wpf中popup跟随鼠标移动显示

    最近在做一个画图工具,里面有一个功能是需要实现,当鼠标移动的时候在,鼠标的旁边显示坐标信息. 第一反应是想到了tooltip,但是tooltip有许多的限制,查询资料的过程中看到了popup,popu ...

  7. EasyPusher安卓Android手机直播推送之RTSP流媒体协议流程

    EasyPusher移动端推送同我们平时用的RTSP直播推送流程一样,都是采用标准RTSP/RTP推送流程:ANNOUNCE->SETUP->PLAY->RTP/RTCP->T ...

  8. mongodb学习之:数据库命令以及固定集合

    如何我们要删除一个集合,可以采用db.test.drop()的方式,其实在这背后,这个函数运行的是drop命令.可以用runCommand达到同样的效果. 我们首先新建一个集合作为测试使用: > ...

  9. windows下使用DOS命令删除大文件

    首先进入DOS命令窗口: 1.进入到将要删除的目录: 执行命令: rmdir /s/q 文件夹名称 2.删除指定文件: 进入文件的目录下,找到要删除的文件名称+扩展名:例如111.jpg执行命令: d ...

  10. ABAP DEMO-2018

    sap Program DEMO 介绍 Program Description BALVBT01 Example SAP program for displying multiple ALV repo ...