1 fork/join 框架包括多个类
1 ForkJoinPool 线程池类 ,
2 ForkJoinTask 任务类
任务子类有2种
//有返回值
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {}
//无返回值
public abstract class RecursiveAction extends ForkJoinTask<Void> {}
2 接下来我们看一个例子:(求两数之间所有数之和,如1-100——>5050):
class ForkJoinSumCalculate extends RecursiveTask<Long>{
private static final long serialVersionUID = -1812835340478767238L;
private long start;
private long end;
private static final long THURSHOLD = 10000L; //临界值
public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
//小于临界值,则不进行拆分,直接计算初始值到结束值之间所有数之和
if(length <= THURSHOLD){
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{ //大于临界值,取中间值进行拆分,递归调用
long middle = (start + end) / 2;
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
left.fork(); //进行拆分,同时压入线程队列
ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
right.fork(); //
return left.join() + right.join();
}
}
}
public static void main(String[] args) {
Instant start = Instant.now();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
Long sum = pool.invoke(task);
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());
}
从例子上看,程序自定义了一个Forkjoin线程池 ,然后定义了一个 ForkJoinTask ,然后使用pool.invoke(task) 方法执行任务
这样线程池内部 取出一个空闲线程 就会去执行这个任务,然后发现这个任务可以分解成许多小任务,然后就执行分解逻辑,并且把小任务放入线程池队列,这样其他线程就可以获取小任务执行了。最后每个小任务的结果 合并 成一个大的结果。
3 换位思考 ThreadPoolExecotor 的实现逻辑
如果不用fork/join 框架而自定义拆分逻辑然后每部分让一个线程执行的话,伪代码应该是这个样子
List<Future> list=new ArrayList();
for(int i =0 i<group.size;i++){
list.add(executorservice.submit(task))
}
然后遍历list ,获取执行结果。这样写起来麻烦,而且还要通过遍历获取结果,不好,
总结 如果任务需要拆分的需求,则可以使用fork/join
4 另外 fork/join 还有一个特性 任务窃取。
有3个任务,1个简单1个中等1个复杂,
如果把这三个交给ThreadPoolExecutor 的时候 如果线程池有3个线程,则最快执行完时间是复杂任务的执行时间。
如果把这三个任务交给ForkJoinPool(同样有3个线程) ,然后把三个任务拆分放入任务队列,则最快执行时间 ,就是三个任务的平均时间,因为 任务拆分之后,三个线程执行的都是子任务,每个线程都可以执行其他父任务拆分出来的子任务。
5 综上所述,Fork/Join 模式有自己的适用范围。如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能够获得最终的答案,那么这个应用就适合用 Fork/Join 模式来解决.
ThreadPoollExector 适合于 不能拆分的任务;
6 ForkJoinPool api 和 简单介绍:
使用的工作窃取的方式能够在最大方式上充分利用CPU的资源,一般流程是fork分解,join结合。本质是将一个任务分解成多个子任务,每个子任务用单独的线程去处理,主要几个常用方法有
fork( ForkJoinTask) | 异步执行一个线程 |
join( ForkJoinTask) | 等待任务完成并返回执行结果 |
execute( ForkJoinTask) | 执行不带返回值的任务 |
submit( ForkJoinTask) | 执行带返回值的任务 |
invoke( ForkJoinTask) | 执行指定的任务,等待完成,返回结果。 |
invokeAll(ForkJoinTask) | 执行指定的任务,等待完成,返回结果。 |
shutdown() | 执行此方法之后,ForkJoinPool 不再接受新的任务,但是已经提交的任务可以继续执行。如果希望立刻停止所有的任务,可以尝试 shutdownNow() 方法。 |
awaitTermination(int, TimeUnit.SECONDS) | 阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束。 |
compute() | 执行任务的具体方法 |
Runtime.getRuntime().availableProcessors() | 获取CPU个数的方法 |
它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。这一点与当前很火的大数据处理框架Hadoop的map/reduce思想非常类似,但是他们的使用场合却不一样,ForkJoinPool适合在一台PC多核CPU上运行,而hadoop则适合在分布式环境中进行大规模集群部署。 典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。
那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
7 自动并行化(Automatic Parallelization)
在Java 8中,引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。
Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。
当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。
比如下面的代码用来遍历列表中的元素并执行需要的计算:
Stream<Integer> stream = arrayList.parallelStream();stream.forEach(a -> {String symbol = StockPriceUtils.makeSymbol(a);StockPriceHistory sph = new StockPriceHistoryImpl(symbol, startDate, endDate, entityManager);});
对于列表中的元素的计算都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的通用线程池处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。如果需要调整线程数量,可以通过设置系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
下面的一组数据用来比较使用ThreadPoolExecutor和ForkJoinPool中的通用线程池来完成上面简单计算时的性能:
线程数 | ThreadPoolExecutor(秒) | ForkJoinPool Common Pool(秒) |
---|---|---|
1 | 255.6 | 135.4 |
2 | 134.8 | 110.2 |
4 | 77.0 | 96.5 |
8 | 81.7 | 84.0 |
16 | 85.6 | 84.6 |
注意到当线程数为1,2,4时,性能差异的比较明显。线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor的性能十分接近。
出现这种现象的原因是,forEach方法用了一些小把戏。它会将执行forEach本身的线程也作为线程池中的一个工作线程。因此,即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此在使用forEach的时候,线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。
所以当ForkJoinPool通用线程池实际需要4个工作线程时,可以将它设置成3,那么在运行时可用的工作线程就是4了。
-
当需要处理递归分治算法时,考虑使用ForkJoinPool。
-
仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。
-
Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。