ForkJoin框架的RecursiveTask和ForkJoinPool的使用案例

詩水人間發表於2020-11-26

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

相關文章