使用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,因为用的不好反而降低效率。

上面的案例是利用了有返回值的抽象类,实际还可以使用RecursiveActionRecursiveActionRecursiveTask区别在于实现方法compute有无返回值

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页