使用ForkJoin解决0到1百亿的求和问题-----一次测试和调优记录,掌握ForkJoinPool的核心用法
ForkJoin框架是jdk7产生的一个新的并发框架,从其名字得知两个词fork()
拆分、join()
合并
就是利用拆分合并的思想,将一个大任务先拆分好,直到不能拆分为止,然后完成任务,最终将结果合并。
下面代码是计算0-1百亿的和的三种计算方式。
结果是肯定超过了Long所能表示的值,但没关系,我们只是举个例子,结果的值不重要,只需要3个结果一致即可
先看一遍然后看后面解说
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
public class Demo {
public static void main(String[] args) {
long max=100_0000_0000L; //计算0到这个值的和
// 方式一,直接for暴力算
long start = System.currentTimeMillis();
long result = 0;
for (long i = 0; i <= max; i++) {
result += i;
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "毫秒,结果是" + result);
// 方式二。forkjoin
long start2 = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinCalculate forkJoinCalculate = new ForkJoinCalculate(0L, max);
Long result2 = pool.invoke(forkJoinCalculate);
long end2 = System.currentTimeMillis();
System.out.println("耗时:" + (end2 - start2) + "毫秒,结果是" + result2);
// 方式三。java8利用流的计算
long start3 = System.currentTimeMillis();
LongStream longStream = LongStream.rangeClosed(0, max);
long result3 = longStream.parallel().sum();
long end3 = System.currentTimeMillis();
System.out.println("耗时:" + (end3 - start3) + "毫秒,结果是" + result3);
}
}
class ForkJoinCalculate extends RecursiveTask<Long> {
private Long start;
private Long end;
private static Long THRESHOLD = 10000L;
public ForkJoinCalculate(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long length = end - start;
if (length <= THRESHOLD) {
long result = 0;
for (long i = start; i <= end; i++) {
result += i;
}
return result;
} else {
long middle = (start + end) / 2;
ForkJoinCalculate left = new ForkJoinCalculate(start, middle);
left.fork();
ForkJoinCalculate right = new ForkJoinCalculate(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
执行main得到的结果如下
耗时:3355毫秒,结果是-5340232226128654848
耗时:1566毫秒,结果是-5340232216128654848
耗时:1041毫秒,结果是-5340232216128654848
从这个结果来看,三种方式结果是一样的因此都没有丢值。
方式一、采用单线程的for直接算得到这个就不用我说什么。所有耗时都在计算,但他是单线程的
方式三、采用java8的并行流得到结果
方式二、采用RecursiveTask
(递归任务的意思),和ForkJoinPool
。初次看来是java8的并行流效率更高,但我们调整一下参数THRESHOLD
的值看能否超过java8,方式二耗时的地方有两块,一是拆分任务需要时间,二计算小任务需要时间。
我将值改成了THRESHOLD = 10_0000_0000L
,结果发现耗时超过了java8的并行流
第一次执行结果:
耗时:3369毫秒,结果是-5340232226128654848
耗时:835毫秒,结果是-5340232216128654848
耗时:1095毫秒,结果是-5340232216128654848
第二次执行结果
耗时:3369毫秒,结果是-5340232226128654848
耗时:1066毫秒,结果是-5340232216128654848
耗时:1057毫秒,结果是-5340232216128654848
第三次执行结果
耗时:3362毫秒,结果是-5340232226128654848
耗时:833毫秒,结果是-5340232216128654848
耗时:1057毫秒,结果是-5340232216128654848
第四次执行结果
耗时:3369毫秒,结果是-5340232226128654848
耗时:829毫秒,结果是-5340232216128654848
耗时:1052毫秒,结果是-5340232216128654848
取了那么多次结果会发现ForkJoinPool的效率比java8的并行流效率高,如果适当的调整应该可以得到一个最佳的效果。
以上是关于ForkJoinPool的优点下面谈谈缺点
我们将max的值调小一点,适当的调整THRESHOLD值
,看下那种效率高
如下一组值
long max = 1_0000_0000L;
private static Long THRESHOLD = 1_0000L;
执行的结果如下
第一次:
耗时:36毫秒,结果是5000000050000000
耗时:64毫秒,结果是5000000050000000
耗时:32毫秒,结果是5000000050000000
第二次:
耗时:36毫秒,结果是5000000050000000
耗时:52毫秒,结果是5000000050000000
耗时:30毫秒,结果是5000000050000000
第三次:
耗时:38毫秒,结果是5000000050000000
耗时:55毫秒,结果是5000000050000000
耗时:26毫秒,结果是5000000050000000
会发现java8的效率最高,forkjoin最差,原因很简单,forkjoin拆分任务需要时间,如果拆的更细,那么拆分的耗时也就会更大
经过分析,forkjoin它能处理一些重复,并且量很大的任务,利用拆分合并的思想将大任务化小,通过适当的调整任务的最小粒度,可以优化代码的执行效率。
根据上面的案例,会发现计算小的数例子中java8的并行流计算效率最佳。二计算大的数用forkjoin效率最佳。
综合考虑
关于计算0到1百亿的和,可以考虑forkjoin + java8的并行流,也许会得到更好的结果值。也就是将for循环改成并行流
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
public class Demo {
public static void main(String[] args) {
long max=100_0000_0000L;
// 方式一,直接for暴力算
long start = System.currentTimeMillis();
long result = 0;
for (long i = 0; i <= max; i++) {
result += i;
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "毫秒,结果是" + result);
// 方式二。forkjoin
long start2 = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinCalculate forkJoinCalculate = new ForkJoinCalculate(0L, max);
Long result2 = pool.invoke(forkJoinCalculate);
long end2 = System.currentTimeMillis();
System.out.println("耗时:" + (end2 - start2) + "毫秒,结果是" + result2);
// 方式三。java8利用流的计算
long start3 = System.currentTimeMillis();
LongStream longStream = LongStream.rangeClosed(0, max);
long result3 = longStream.parallel().sum();
long end3 = System.currentTimeMillis();
System.out.println("耗时:" + (end3 - start3) + "毫秒,结果是" + result3);
}
}
class ForkJoinCalculate extends RecursiveTask<Long> {
private Long start;
private Long end;
private static Long THRESHOLD = 10_0000_0000L;
public ForkJoinCalculate(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long length = end - start;
if (length <= THRESHOLD) {
LongStream longStream = LongStream.rangeClosed(start, end);
return longStream.parallel().sum();// java8并行流计算
} else {
long middle = (start + end) / 2;
ForkJoinCalculate left = new ForkJoinCalculate(start, middle);
left.fork();
ForkJoinCalculate right = new ForkJoinCalculate(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
在我的实际测试过程中,发现这种做法并没有想象中的快,主要是不太稳,值有些变化,而java8的并行流计算比较稳,始终都在900-1000毫秒左右,而forkjoin好的时候是770坏的时候1600,一般在900-1000左右,还没有前面forkjoin+for
调整条件值得800多好。
因此像这种值得计算推荐使用java8的并行流计算比较稳妥,如果是其它类,则需要权衡一下到底要不要使用forkjoin,因为用的不好反而降低效率。
上面的案例是利用了有返回值的抽象类,实际还可以使用RecursiveAction
,RecursiveAction
与RecursiveTask
区别在于实现方法compute
有无返回值
本文地址:https://blog.csdn.net/qq_41813208/article/details/110153700