java ForkJoin 简单使用示例, 求和拆分为多个 线程执行,已提高效率

 1 import java.util.concurrent.ForkJoinPool;
 2 import java.util.concurrent.ForkJoinTask;
 3 import java.util.stream.LongStream;
 4 
 5 /**
 6  * @author 
 7  * @version V1.0
 8  * @Package 
 9  * @date 2020/5/14 10:13
10  */
11 // 任务合并 框架 RecursiveTask
12 public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {
13     //  要求和的数组
14     private final long[] numbers;
15     // 子任务的数组起始位置 和截至位置
16     private final int start;
17     private final int end;
18     // 数组内容低于 该值 则不再拆分
19     public static final long THRESHOLD = 10000;
20 
21 
22     private ForkJoinSumCalculator(long[] numbers, int start, int end) {
23         this.numbers = numbers;
24         this.start = start;
25         this.end = end;
26     }
27 
28     public ForkJoinSumCalculator(long[] numbers) {
29        this(numbers,0,numbers.length);
30     }
31 
32 
33     // 复写方法求和
34     @Override
35     protected Long compute() {
36         int length = end - start;
37         //如果内容 小于指定阈值 则不做拆分  直接返回求和结果
38         if (length <= THRESHOLD) {
39             return computeSequentially();
40         }
41         // 大于阈值 则 拆分成两个 不同的任务子进程 单独求和
42         ForkJoinSumCalculator leftTask =
43                 new ForkJoinSumCalculator(numbers, start, start + length/2);
44         // 利用一个新的线程来计算该子任务的求和
45         leftTask.fork();
46         ForkJoinSumCalculator rightTask =
47                 new ForkJoinSumCalculator(numbers, start + length/2, end);
48         // 递归调用,  方面 多次拆分,
49         Long rightResult = rightTask.compute();
50         Long leftResult = leftTask.join();
51         return leftResult + rightResult;
52     }
53 
54     private long computeSequentially() {
55         long sum = 0;
56         for (int i = start; i < end; i++) {
57 
58                 sum += numbers[i];
59             }
60             return sum;
61 
62         }
63 
64 
65         // 对外调用  接口
66     public static long forkJoinSum(long n) {
67         long[] numbers = LongStream.rangeClosed(1, n).toArray();
68         ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
69         return new ForkJoinPool().invoke(task);
70     }
71 
72 
73     public long[] getNumbers() {
74         return numbers;
75     }
76 
77     public int getStart() {
78         return start;
79     }
80 
81     public int getEnd() {
82         return end;
83     }
84 
85     public static long getTHRESHOLD() {
86         return THRESHOLD;
87     }
88 }

 

虽然分支/合并框架还算简单易用,不幸的是它也很容易被误用。以下是几个有效使用它的 最佳做法。  对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子 任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂, 因为每个子任务都必须等待另一个子任务完成才能启动。
 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直 接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用 它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为 其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面 看栈跟踪(stack trace)来找问题,但放在分支合并计算上就不行了,因为调用compute 的线程并不是概念上的调用方,后者是调用fork的那个。
 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计 算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时 有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方 法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出 同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就 像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编 译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么 做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死 码分析——删去从未被使用的计算)。
对于分支/合并拆分策略还有最后一点补充:你必须选择一个标准,来决定任务是要进一步
拆分还是已小到可以顺序求值。我们会在下一节中就此给出一些提示。

posted @ 2020-05-14 11:06  kuailecs54  阅读(402)  评论(0)    收藏  举报