前面我们站在任务分工的角度来介绍java工具类,今天来介绍一下使用场景:
分治:顾名思义就是分而治之,把一个问题分解成多个子问题,把子问题分解成更小的子问题,知道可以简单直接求解。算法领域中,归并排序,快速排序,二分查找等都有涉及,大数据知名框架MapReduce背后的思想也是分治。
分治任务模型:
分两个阶段:一个是任务分解;另一个是结果合并。
当然,java也支持这种思想,java并发包中提供一种Fork/Join的并行计算框架,就是支持分治任务模型的。
Fork/Join是一个并行计算框架,主要用来支持分治任务模型的,这个计算框架中Fork对应的分治任务模型中的任务分解,join对应的是任务合并。
Fork/Join计算框架主要包括两部分,一部分是分治任务的线程池ForkJoinPool,另一部分是分治任务ForkJoinTask。有点类似于线程池ThreadPoolExecutor和Runable的关系。
ForkJoinTask是一个抽象类,它的方法很多,最核心的方法是fork()方法和join()方法。fork():会异步执行一个子任务,而join():会阻塞当前线程等待子任务执行结果。
ForkJoinTask有两个子类——RecursiveAction和RecursiveTask,看名字应该能看出来都是通过递归处理分治任务的。这两个子类都定义了抽象方法compute(),不过区别是RecursiveAction定义的compute没有返回值,而RecursiveTask定义的compute有返回值,同样这两个子类也是抽象方法,需要子类继承使用。
static void main(String[] args){
//创建分治任务线程池
ForkJoinPool fjp = new ForkJoinPool(4);
//创建分治任务
Fibonacci fib = new Fibonacci(30);
//启动分治任务
Integer result = fjp.invoke(fib);
//输出结果
System.out.println(result);
}
//递归任务
static class Fibonacci extends RecursiveTask<Integer>{
final int n;
Fibonacci(int n){this.n = n;}
protected Integer compute(){
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
//创建子任务
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
//等待子任务结果,并合并结果
return f2.compute() + f1.join();
}
}
前面我们知道线程池ThreadPoolExecutor本质上是生产者消费者模式,内部只有一个任务队列,消费者和生产者通过任务队列通信,线程池可以有多个工作线程,但是这些工作线程都共享同一个任务队列。
ForkJoinPool本质上也是生产者消费者模式,但是更加智能,可以参考下面的图来理解ForkJoinPool的工作原理。
学习 MapReduce 有一个入门程序,统计一个文件里面每个单词的数量,下面我们来看看如何用 Fork/Join 并行计算框架来实现。
我们可以先用二分法递归地将一个文件拆分成更小的文件,直到文件里只有一行数据,然后统计这一行数据里单词的数量,最后再逐级汇总结果。
下面的示例程序用一个字符串数组 String[] fc 来模拟文件内容,fc 里面的元素与文件里面的行数据一一对应。
关键的代码在 compute() 这个方法里面,这是一个递归方法,前半部分数据 fork 一个递归任务去处理(关键代码 mr1.fork()),后半部分数据则在当前任务中递归处理(mr2.compute())。
static void main(String[] args){
String[] fc = {
"hello world",
"hello me",
"hello fork" ,
"hello join",
"fork join in world"};
//创建ForkJoin线程池
ForkJoinPool fjp = new ForkJoinPool(3);
//创建任务
MR mr = new MR( fc, 0, fc.length);
//启动任务
Map<String, Long> result = fjp.invoke(mr);
//输出结果
result.forEach((k, v)-> System.out.println(k+":"+v));
}
//MR模拟类
static class MR extends RecursiveTask<Map<String, Long>> {
private String[] fc;
private int start, end;
//构造函数
MR(String[] fc, int fr, int to){
this.fc = fc;
this.start = fr;
this.end = to;
}
@Override protected
Map<String, Long> compute(){
if (end - start == 1) {
return calc(fc[start]);
} else {
int mid = (start+end)/2;
MR mr1 = new MR( fc, start, mid);
mr1.fork();
MR mr2 = new MR( fc, mid, end);
//计算子任务,并返回合并的结果
return merge(mr2.compute(), mr1.join());
}
}
//合并结果
private Map<String, Long> merge( Map<String, Long> r1, Map<String, Long> r2) {
Map<String, Long> result = new HashMap<>();
result.putAll(r1);
//合并结果
r2.forEach((k, v) -> {
Long c = result.get(k);
if (c != null)
result.put(k, c+v);
else
result.put(k, v);
});
return result;
}
//统计单词数量
private Map<String, Long> calc(String line) {
Map<String, Long> result = new HashMap<>();
//分割单词
String [] words = line.split("\\s+");
//统计单词数量
for (String w : words) {
Long v = result.get(w);
if (v != null)
result.put(w, v+1);
else
result.put(w, 1L);
}
return result;
}
}
ForkJoin并行计算框架的核心组件是ForkJoinPool,ForkJoinPool支持任务窃取机制,这样能够让所有线程的任务工作量均等,不会出现有的线程忙,有的线程空闲。性能很好。
Java 1.8 提供的 Stream API 里面并行流也是以 ForkJoinPool 为基础的。不过,默认情况下所有的并行流计算都共享一个 ForkJoinPool,**这个共享的 ForkJoinPool 默认的线程数是 CPU 的核数;**如果所有的并行流计算都是 CPU 密集型计算的话,完全没有问题,但是如果存在 I/O 密集型的并行流计算,那么很可能会因为一个很慢的 I/O 计算而拖慢整个系统的性能。所以建议用不同的 ForkJoinPool 执行不同类型的计算任务。