看Fork/Join框架的时候发现这玩意儿跟归并排序简直就是绝配。
归并排序
归并排序的思想是才有分治策略将大问题拆分成一些小问题递归求解,然后将各个小问题的解合并得到最终结果。
拆分合并步骤:
一:拆分:
1)一个大无序序列从中间拆分成2个无序序列;
2)上一步拆分出的2个无序序列分别又拆分成2个无序序列;
3)重复步骤2,直到拆分后的序列只包含一两个数的序列。
二:求解合并:
1)选择2个相邻的数进行排序构成一个短的有序序列;
2)选择2个相邻的有序序列排序构成一个大的有序序列;
3)重复步骤2,直到最终组合成一个完整的有序序列。
Fork/Join
JDK1.7出现的一个线程池框架。一个任务可以选择直接执行或者是拆分成小任务再分别执行,最后将结果汇总。
该框架的原理描述解析网上很多就不说了,直接上示例代码:
编写自定义的任务类:MyTask
// 继承RecursiveTask,这个是ForkJoinPool线程池执行任务的抽象基类;
//顶级基类是ForkJoinTask,一般继承在2个子抽象类中选择:
//RecursiveTask(有返回值的任务)、RecursiveAction(无返回值的任务)
class MyTask extends RecursiveTask<int []> {
private int [] source;
public MyTask(int[] data) {
this.source = data;
}
@Override
//重写这个具体任务的执行方法
protected int [] compute() {
int sourceLen = source.length;
if(sourceLen > 2) { // 如果数组长度大于2,那么就继续拆分任务
int midIndex = sourceLen / 2;//从中间拆分
// 拆分成两个子任务
MyTask task1 = new MyTask(Arrays.copyOf(source, midIndex));
task1.fork();// fork() :将这个任务(自己)加入到线程池里面
MyTask task2 = new MyTask(Arrays.copyOfRange(source, midIndex , sourceLen));
task2.fork();
int result1[] = task1.join();//join():等待线程池执行任务后返回的结果
int result2[] = task2.join();
// 将两个有序的数组,合并成一个有序的数组
int mer[] = joinInts(result1 , result2);
return mer; // 返回2个数组合并后的数组
} else { // 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
// 如果数组中只有一个元素,或者是数组中的元素已经有序了直接返回
if(sourceLen == 1 || source[0] <= source[1]) {
return source;
} else {
// 否则将数组排序(只有2个元素,直接交换位置就排序了)
int result[] = new int[sourceLen];
result[0] = source[1];
result[1] = source[0];
return result;
}
}
}
// 将2个有序数组 合并成一个有序数组的方法
private int[] joinInts(int array1[] , int array2[]) {
//合并后的数组
int[] result= new int[array1.length + array2.length];
int array1Len = array1.length;
int array2Len = array2.length;
int destLen = result.length;
int array1Index = 0 , array2Index = 0 ;
for(int index = 0 ; index < destLen ; index++) {
// 注意这里的三目运算,如果数组1的元素都比数组2的小,数组1的下标会一直后移,会越界
int value1 = array1Index >= array1Len?Integer.MAX_VALUE:array1[array1Index];
int value2 = array2Index >= array2Len?Integer.MAX_VALUE:array2[array2Index];
// 如果数组1的值比数组2的值小,那么该位置就存数组1的值,并且数组1的下标后移
if(value1 < value2) {
array1Index++;
result[index] = value1;
} else {
array2Index++;
result[index] = value2;
}
}
//返回合并后的结果数组
return result;
}
}
测试使用:
public static void main(String[] args) {
//生成一亿个[1,9999]的无序数组
int[] data = new Random().ints(1, 10000).limit(100000000).toArray();
long m1 = System.currentTimeMillis();
//创建一个默认配置的线程池,可以自定义参数
ForkJoinPool pool = new ForkJoinPool();
//创建一个大任务
MyTask task = new MyTask(data);
//丢到线程池里面去,等待获取结果
int[] invoke = pool.invoke(task);
// pool.execute(task);pool.submit(task); // 这样也可以执行任务
long m2 = System.currentTimeMillis();
System.out.println("数组长度:" + data.length + ",耗时:" +(m2-m1) + "ms");
pool.shutdown();
}
执行结果:
数组长度:100000000,耗时:7704ms
一亿数据排序基本稳定在7-8秒。
常规的归并排序运行结果:数组长度:100000000,耗时:14962ms
数据量大时,比常规的归并排序写法还是快很多。
一亿数据:
一千万数据:
一百万数据:
再来个数组求和的示例代码:
class Task extends RecursiveTask<Long>{
private int[] arr;
private int start;
private int end;
public Task(int[] arr, int start, int end) {
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
//这个拆分的分界线决定了执行的最终效率,如果拆分得不好效率反而会降低
if(end - start < 1000){ // 拆分到一千个元素以内就直接求和,否则继续拆分任务
Long sum = 0L;
for(int i=start;i<end;i++){
sum += arr[i];
}
// System.out.println(Thread.currentThread().getName() + " 执行结果:" + sum);
return sum;
}else{
int mid = (start + end) / 2;
Task left = new Task(ints, start, mid);
left.fork();
Task r = new Task(ints, mid, end);
r.fork();
// System.out.println(Thread.currentThread().getName() + " 再次拆分任务" );
return left.join() + r.join() ;
}
}
}
JDK1.8中的并行流底层就是用的Fork/Join框架,它把任务拆分优化得很好才会很快。