欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

使用ForkJoin解决0到1百亿的求和问题-----一次测试和调优记录,掌握ForkJoinPool的核心用法

程序员文章站 2022-04-29 14:30:17
ForkJoin框架是jdk7产生的一个新的并发框架,从其名字得知两个词fork()拆分、join()合并就是利用拆分合并的思想,将一个大任务先拆分好,直到不能拆分为止,然后完成任务,最终将结果合并。下面代码是计算0-1百亿的和的三种计算方式。结果是肯定超过了Long所能表示的值,但没关系,我们只是举个例子,结果的值不重要,只需要3个结果一致即可先看一遍然后看后面解说import java.util.concurrent.ForkJoinPool;import java.util.concurr...

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有无返回值

本文地址:https://blog.csdn.net/qq_41813208/article/details/110153700