先把一个庞大的数组进行递归分解,把拆分的数组排好序,之后把拆分排好序的数组进行有序的合并,必须住的问题就是,递归拆分的阈值,比如当数组长度拆分到10000时候就不拆了,不能无限制的拆分,如果栈帧入栈太多,而受栈大小的限制会发生栈溢出
归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。
数组排序归并排序图解
public class ArraySortTest {
public static void main(String[] args) {
//生成两千万测试数组 用于归并排序
int[] arrayToSortByMergeSort = buildRandomIntArray(20000000);
long startTime = System.nanoTime();
int[] mergeSort = mergeSort(arrayToSortByMergeSort, Runtime.getRuntime().availableProcessors());
long duration = System.nanoTime()-startTime;
System.out.println("单线程归并排序时间: "+(duration/(1000f*1000f))+"毫秒");
}
public static int[] mergeSort(int[] intArray, int threshold) {
if (intArray.length < threshold) {
// 小于阈值之后不再进行拆分,把小数组顺序整理好(这里先采用冒泡排序 冒泡的思路就是把数组中最大的值往后移)
int temp;
for (int i = 0; i < intArray.length - 1; i++) {
// 判断是否发生交换
boolean isSwap = false;
for (int j = 0; j < intArray.length - 1 - i; j++) {
if (intArray[j] > intArray[j+1]) {
temp = intArray[j];
intArray[j] = intArray[j+1];
intArray[j + 1] = temp;
isSwap = true;
}
}
if (!isSwap) {
// 如果没有发生交换 说明数组是有序的
break;
}
}
return intArray;
}
// 大于阈值 继续拆分
int midpoint = intArray.length / 2;
int[] leftArray = Arrays.copyOfRange(intArray, 0, midpoint);
int[] rightArray = Arrays.copyOfRange(intArray, midpoint, intArray.length);
// 递归进行拆分
leftArray = mergeSort(leftArray, threshold);
rightArray = mergeSort(rightArray, threshold);
// 合并排序结果
return merge(leftArray, rightArray);
}
/**
* 合并
* @param leftArray
* @param rightArray
* @return
*/
private static int[] merge(final int[] leftArray, final int[] rightArray) {
// 新建一个新的数组用于存储返回结果集
int[] mergeArray = new int[leftArray.length + rightArray.length];
int mergeIndex = 0, leftIndex = 0, rightIndex = 0;
while (leftIndex < leftArray.length && rightIndex < rightArray.length) {
if (leftArray[leftIndex] <= rightArray[rightIndex]) {
mergeArray[mergeIndex] = leftArray[leftIndex];
leftIndex++;
} else {
mergeArray[mergeIndex] = rightArray[rightIndex];
rightIndex++;
}
mergeIndex++;
}
// 两个数组中最后总会有一个数组剩下一个元素 把最后一个元素加到mergeArray中
while (leftIndex < leftArray.length) {
mergeArray[mergeIndex] = leftArray[leftIndex];
leftIndex++;
mergeIndex++;
}
while (rightIndex < rightArray.length) {
mergeArray[mergeIndex] = rightArray[rightIndex];
rightIndex++;
mergeIndex++;
}
return mergeArray;
}
/**
* 随机生成指定size大小的数组
* @param size
* @return
*/
private static int[] buildRandomIntArray(final int size) {
int[] arrayToCalculateSumOf = new int[size];
Random generator = new Random();
for (int i = 0; i < arrayToCalculateSumOf.length; i++) {
arrayToCalculateSumOf[i] = generator.nextInt(100000000);
}
return arrayToCalculateSumOf;
}
}
打印结果:单线程归并排序时间: 4081.0142毫秒
public class MergeSortTask extends RecursiveAction {
private final int threshold; //拆分的阈值,低于此阈值就不再进行拆分
private int[] arrayToSort; //要排序的数组
public MergeSortTask(final int[] arrayToSort, final int threshold) {
this.arrayToSort = arrayToSort;
this.threshold = threshold;
}
@Override
protected void compute() {
//拆分后的数组长度小于阈值,直接进行排序
if (arrayToSort.length <= threshold) {
// 调用jdk提供的排序方法
Arrays.sort(arrayToSort);
return;
}
// 对数组进行拆分
int midpoint = arrayToSort.length / 2;
int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);
MergeSortTask leftTask = new MergeSortTask(leftArray, threshold);
MergeSortTask rightTask = new MergeSortTask(rightArray, threshold);
//调用任务,阻塞当前线程,直到所有子任务执行完成
invokeAll(leftTask,rightTask);
//提交任务
// leftTask.fork();
// rightTask.fork();
// //合并结果
// leftTask.join();
// rightTask.join();
// 合并排序结果
arrayToSort = ArraySortTest.merge(leftTask.getSortedArray(), rightTask.getSortedArray());
}
public int[] getSortedArray() {
return arrayToSort;
}
public static void main(String[] args) {
int[] arrayToSortByMergeSort = ArraySortTest.buildRandomIntArray(20000000);
//利用forkjoin排序
MergeSortTask mergeSortTask = new MergeSortTask(arrayToSortByMergeSort, Runtime.getRuntime().availableProcessors());
//构建forkjoin线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
long startTime = System.nanoTime();
//执行排序任务
forkJoinPool.invoke(mergeSortTask);
long duration = System.nanoTime()-startTime;
System.out.println("forkjoin排序时间: "+(duration/(1000f*1000f))+"毫秒");
}
}
打印结果:forkjoin排序时间: 1176.6014毫秒
Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。
Fork/Join框架的应用场景包括以下几个方面:
Fork/Join框架特别适用于递归分解型的任务,例如排序、归并、遍历等。这些任务通常可以将大的任务分解成若干个子任务,每个子任务可以独立执行,并且可以通过归并操作将子任务的结果合并成一个有序的结果。
Fork/Join框架还可以用于数组的处理,例如数组的排序、查找、统计等。在处理大型数组时,Fork/Join框架可以将数组分成若干个子数组,并行地处理每个子数组,最后将处理后的子数组合并成一个有序的大数组。
Fork/Join框架还可以用于并行化算法的实现,例如并行化的图像处理算法、并行化的机器学习算法等。在这些算法中,可以将问题分解成若干个子问题,并行地解决每个子问题,然后将子问题的结果合并起来得到最终的解决方案。
Fork/Join框架还可以用于大数据处理,例如大型日志文件的处理、大型数据库的查询等。在处理大数据时,可以将数据分成若干个分片,并行地处理每个分片,最后将处理后的分片合并成一个完整的结果。
Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。
ForkJoinPool是Fork/Join框架中的线程池类,它用于管理Fork/Join任务的线程。ForkJoinPool类包括一些重要的方法,例如submit()、invoke()、shutdown()、awaitTermination()等,用于提交任务、执行任务、关闭线程池和等待任务的执行结果。ForkJoinPool类中还包括一些参数,例如线程池的大小、工作线程的优先级、任务队列的容量等,可以根据具体的应用场景进行设置。
构造器方法
ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:
//获取处理器数量
int processors = Runtime.getRuntime().availableProcessors();
//构建forkjoin线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(processors);
任务提交是ForkJoinPool的核心能力之一,提交任务有三种方式
返回值 |
方法 |
|
提交异步执行 |
void |
execute(ForkJoinTask task) execute(Runnable task) |
等待并获取结果 |
T |
invoke(ForkJoinTask task) |
提交执行获取Future结果 |
ForkJoinTask |
submit(ForkJoinTask task) submit(Callable task) submit(Runnable task) submit(Runnable task, T result) |
ForkJoinPool采用工作窃取算法来提高线程的利用率,而普通线程池则采用任务队列来管理任务。在工作窃取算法中,当一个线程完成自己的任务后,它可以从其它线程的队列中获取一个任务来执行,以此来提高线程的利用率。
ForkJoinPool可以将一个大任务分解为多个小任务,并行地执行这些小任务,最终将它们的结果合并起来得到最终结果。而普通线程池只能按照提交的任务顺序一个一个地执行任务。
ForkJoinPool会根据当前系统的CPU核心数来自动设置工作线程的数量,以最大限度地发挥CPU的性能优势。而普通线程池需要手动设置线程池的大小,如果设置不合理,可能会导致线程过多或过少,从而影响程序的性能。
ForkJoinPool适用于执行大规模任务并行化,而普通线程池适用于执行一些短小的任务,如处理请求等。
ForkJoinTask是Fork/Join框架中的抽象类,它定义了执行任务的基本接口。用户可以通过继承ForkJoinTask类来实现自己的任务类,并重写其中的compute()方法来定义任务的执行逻辑。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:
ForkJoinTask 最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。
fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。
斐波那契数列指的是这样一个数列:1,1,2,3,5,8,13,21,34,55,89... 这个数列从第3项开始,每一项都等于前两项之和。
public class Fibonacci extends RecursiveTask {
final int n;
Fibonacci(int n) {
this.n = n;
}
/**
* 重写RecursiveTask的compute()方法
* @return
*/
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();
}
public static void main(String[] args) {
//构建forkjoin线程池
ForkJoinPool pool = new ForkJoinPool();
Fibonacci task = new Fibonacci(10);
//提交任务并一直阻塞直到任务 执行完成返回合并结果。
int result = pool.invoke(task);
System.out.println(result);
}
}
在上面的例子中,由于递归计算Fibonacci数列的任务数量呈指数级增长,当n较大时,就容易出现StackOverflowError错误。这个错误通常发生在递归过程中,由于递归过程中每次调用函数都会在栈中创建一个新的栈帧,当递归深度过大时,栈空间就会被耗尽,导致StackOverflowError错误。
对于一些递归深度较大的任务,使用Fork/Join框架可能会出现任务调度和内存消耗的问题。
当递归深度较大时,会产生大量的子任务,这些子任务可能被调度到不同的线程中执行,而线程的创建和销毁以及任务调度的开销都会占用大量的资源,从而导致性能下降。
此外,对于递归深度较大的任务,由于每个子任务所占用的栈空间较大,可能会导致内存消耗过大,从而引起内存溢出的问题。
因此,在使用Fork/Join框架处理递归任务时,需要根据实际情况来评估递归深度和任务粒度,以避免任务调度和内存消耗的问题。如果递归深度较大,可以尝试采用其他方法来优化算法,如使用迭代方式替代递归,或者限制递归深度来减少任务数量,以避免Fork/Join框架的缺点。
在ForkJoinPool中使用阻塞型任务时需要注意以下几点:
ThreadPoolExecutor是多个线程对应一个任务队列,ForkJoinPool是每个线程对应一个任务队列
// 入队方法
final void push(ForkJoinTask> task) {
ForkJoinTask>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) { // ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
// 通过ForkJoinTask数组的计算下一个任务所存储的偏移量 然后添加到队列中
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
// 队列指针加1
U.putOrderedInt(this, QTOP, s + 1);
if ((n = s - b) <= 1) {
if ((p = pool) != null)
p.signalWork(p.workQueues, this);
}
else if (n >= m)
growArray();
}
}
// 出队方法
final ForkJoinTask> pop() {
ForkJoinTask>[] a; ForkJoinTask> t; int m;
if ((a = array) != null && (m = a.length - 1) >= 0) {
for (int s; (s = top - 1) - base >= 0;) {
long j = ((m & s) << ASHIFT) + ABASE;
if ((t = (ForkJoinTask>)U.getObject(a, j)) == null)
break;
// CAS方式把所在位置的任务置为空
if (U.compareAndSwapObject(a, j, t, null)) {
// s=top-1 这里可以就看出是后进先出的结构
U.putOrderedInt(this, QTOP, s);
// 把需要出对的任务返回
return t;
}
}
}
return null;
}
// 其它线程窃取任务方法
final ForkJoinTask> poll() {
ForkJoinTask>[] a; int b; ForkJoinTask> t;
while ((b = base) - top < 0 && (a = array) != null) {
int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
t = (ForkJoinTask>)U.getObjectVolatile(a, j);
if (base == b) {
if (t != null) {
// 是通过base计算出队任务在数组中的偏移量 CAS置为空
if (U.compareAndSwapObject(a, j, t, null)) {
// 数组在底部base要加1出队
base = b + 1;
// 返回底部置为空的任务
return t;
}
}
else if (b + 1 == top) // now empty
break;
}
}
return null;
}
Fork/Join是一种基于分治思想的模型,在并发处理计算型任务时有着显著的优势。其效率的提升主要得益于两个方面:
在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。