ForkJoinPool畅想
当使用从Java8 的paralleStream 时,有时候得到的结果跟串行执行的stream直接的结果不同,得知paralleStream是线程不安全的,在网上查了些资料得知如果使用并行stream时,内部任务是采用ForkJoinPool来拆分执行的。所以对ForkJoinPool的使用方式进行记录,方便以后查阅:
使用
Java7 提供了ForkJoinPool 来支持将一个任务拆分成多个"小任务"并行计算 , 再把小任务的执行结果 合并成总的计算结果。 ForkJoinPool是ExecutorService的实现类 , 因此是一种特殊的线程池 。 使用方法:创建了FOrkJoinPool实例之后, 就可以调动ForkJoinPool的submit(ForkJoinTask<T> task) ,或者 invoke(ForkJoinPool<T> task) 方法来执行定时任务。 其中 ForkJoinTask代表一个可以并行、合并的任务 , ForkJoinTask是一个抽象类 , 它还有两个抽象子类, RecusiveAction 和 RecusiveTask , 其中RecusiveTask代表有返回值的任务 , 而 RecusiveAction代表没有返回值的任务 。 下面UML图表示了ForkJoinPool 和 ForkJoinTask之间的关系:
案例
将没有返回的大任务,拆分成多个小任务来执行
package com.github.forkjoinpool;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
/**
* Created with IntelliJ IDEA.
*
* @author: zhubo
* @description: 使用ForkJoinPool完成一个任务的分段执行,简单的打印0-300的数值。用多线程实现并行执行
* @time: 2018年07月28日
* @modifytime:
*/
public class ForkJoinPoolAction {
public static void main(String[] args) throws Exception{
PrintTask task = new PrintTask(0, 300);
//创建实例,并执行分割任务
ForkJoinPool pool = new ForkJoinPool();
pool.submit(task);
//线程阻塞,等待所有任务完成
pool.awaitTermination(2, TimeUnit.SECONDS);
pool.shutdown();
}
}
class PrintTask extends RecursiveAction {
public static final int THRESHOLD = 50;
private int start ;
private int end;
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
/**
* The main computation performed by this task.
*/
@Override
protected void compute() {
if(end - start < THRESHOLD){
for(int i=start;i<end;i++){
System.out.println(Thread.currentThread().getName()+"的i值:"+i);
}
}else {
int middle =(start + end)/2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//并行执行两个“小任务”
left.fork();
right.fork();
}
}
}
通过RecusiveTask 的返回值 , 来对一个长度为100的数组元素进行累加
package com.github.forkjoinpool;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
* Created with IntelliJ IDEA.
*
* @author: zhubo
* @description:
* @time: 2018年07月28日
* @modifytime:
*/
public class ForJoinPoolTask {
public static void main(String[] args) throws Exception{
int[] arr = new int[100];
Random random = new Random();
int total = 0;
for (int i = 0,len = arr.length; i < len ; i++) {
int temp = random.nextInt(20);
total += (arr[i] = temp);
}
System.out.println("初始化数组总和:"+total);
SumTask task = new SumTask(arr, 0, arr.length);
//创建一个通用池,这个是jdk1.8提供的功能
ForkJoinPool pool = ForkJoinPool.commonPool();
//提交分解的SumTask 任务
ForkJoinTask<Integer> future = pool.submit(task);
System.out.println(future.get());
pool.shutdown();
}
}
class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 20;
private int array[];
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if(end - start < THRESHOLD) {
for(int i = start ; i < end ; i++ ){
sum = sum + array[i];
}
return sum;
}else { // 当end -start > threshold , 将大任务拆分成小任务
int middle = (start + end)/2;
SumTask left = new SumTask(array , start ,middle);
SumTask right = new SumTask(array , middle , end);
left.fork();
right.fork();
return left.join() + right.join();
}
}
}
分析
在Java7中引入一种新的线程池:ForkJoinPool 它桶ThreadPoolExecotor一样 , 也实现了Executor和 ExecutorService接口, 它使用了一个无线队列来保存需要执行的任务,而线程数量需要通过构造函数传入 , 如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量被设置为线程数量作为默认值 。
ForkJoinPool主要用来使用分治法 来解决问题, 典型的应用比如快速排序算法。 这里重要点在于, ForkJoinPool需要使用相对较少的线程来处理大量的任务。 比如要对1000万个数据进行排序,那么会将这个任务分割为500万的排序任务,何一个针对这两组500万数据的合并任务 。依此类推,对于500万的数据也会做出同样分割处理 , 到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理, 比如当元素数量小于10时会停止分割 , 转而使用插入排序对他们进行排序 。 那么到最后,所有的任务加起来大概有2000000+个 , 问题的关键在于 , 对于一个任务而言, 只有当它所有的子任务完成之后 , 它才能够被执行 。 所以当使用ThreadPoolExecutor时,使用分治法存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行, 而使用ForkJoinPool 时, 就能够让其中的线程创建新的任务 , 并挂起当前的任务,此时线程就能够从队列中选择子任务去执行。
以上程序关键是fork()和 join()方法, 在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的顺序执行 。
那么使用ThreadPoolExecutor或者ForkJoinPool 会有什么性能的差异呢? 首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具体有父子关系的任务, 比如使用4个线程来完成超过200万个任务 , 但是使用ThreadPoolExecutor时,是不可能完成的, 因为ThreadPoolExecutor中的Thread无法选择优先执行子任务 , 需要完成200万个父子关系的任务时,也需要200万个线程,显然这是不可行的。
ps : ForkJoinPool在执行过程中会创建大量的子任务 , 导致GC进行垃圾回收 , 这些是需要注意的。
参考
- http://ifeve.com/fork-join-2/
- https://www.jianshu.com/p/de025df55363
- https://www.jianshu.com/nb/6823192
- https://www.cnblogs.com/lixuwu/p/7979480.html