文章目录

  • 1.分支/合并框架
  • 2.1 使用 RecursiveTask
  • 2.2运行 ForkJoinSumCalculator
  • 3.1 使用分支/合并框架的最佳做法
  • 3.2 工作窃取


本文较乱,待花时间优化

1.分支/合并框架

分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任
务的结果合并起来生成整体结果。它是 ExecutorService 接口的一个实现,它把子任务分配给
线程池(称为 ForkJoinPool )中的工作线程。

2.1 使用 RecursiveTask

要把任务提交到这个池,必须创建 RecursiveTask 的一个子类,其中 R 是并行化任务(以
及所有子任务)产生的结果类型,或者如果任务不返回结果,则是 RecursiveAction 类型(当
然它可能会更新其他非局部机构)。要定义 RecursiveTask, 只需实现它唯一的抽象方法
compute :
protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成
单个子任务结果的逻辑。正由于此,这个方法的实现类似于下面的伪代码:

if (任务足够小或不可分) {
	顺序计算该任务
} else {
    将任务分成两个子任务
    递归调用本方法,拆分每个子任务,等待所有子任务完成
    合并每个子任务的结果
}

一般来说并没有确切的标准决定一个任务是否应该再拆分,但有几种试探方法可以帮助你做

出这一决定。

想把多个项目合并怎么弄 java java项目分工如何合并_RecursiveTask

为一个数字范围(这里用一个
long[] 数组表示)求和。如前所述,你需要先为 RecursiveTask 类做一个实现,就是下面代码
清单中的 ForkJoinSumCalculator 。

public class ForkJoinSumCalculator
extends java.util.concurrent.RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
private ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask =
new ForkJoinSumCalculator(numbers, start, start + length/2);
leftTask.fork();
ForkJoinSumCalculator rightTask =
new ForkJoinSumCalculator(numbers, start + length/2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {{
sum += numbers[i];
}
return sum;
}
}

现在编写一个方法来并行对前n个自然数求和就很简单了。你只需把想要的数字数组传给
ForkJoinSumCalculator 的构造函数:

public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}

这里用了一个 LongStream 来生成包含前n个自然数的数组,然后创建一个 ForkJoinTask
( RecursiveTask 的父类),并把数组传递给ForkJoinSumCalculator 的公共
构造函数。最后,你创建了一个新的 ForkJoinPool ,并把任务传给它的调用方法 。在
ForkJoinPool 中执行时,最后一个方法返回的值就是 ForkJoinSumCalculator 类定义的任务
结果。

请注意在实际应用时,使用多个 ForkJoinPool 是没有什么意义的。正是出于这个原因,一
般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任
何部分方便地重用了。

2.2运行 ForkJoinSumCalculator

当把 ForkJoinSumCalculator 任务传给 ForkJoinPool 时,这个任务就由池中的一个线程

执行,这个线程会调用任务的 compute 方法。该方法会检查任务是否小到足以顺序执行,如果不

够小则会把要求和的数组分成两半,分给两个新的 ForkJoinSumCalculator ,而它们也由

ForkJoinPool 安排执行。因此,这一过程可以递归重复,把原任务分为更小的任务,直到满足

不方便或不可能再进一步拆分的条件(本例中是求和的项目数小于等于10 000)。这时会顺序计

算每个任务的结果,然后由分支过程创建的(隐含的)任务二叉树遍历回到它的根。接下来会合

并每个子任务的部分结果,从而得到总任务的结果。

想把多个项目合并怎么弄 java java项目分工如何合并_RecursiveTask_02

3.1 使用分支/合并框架的最佳做法

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

3.2 工作窃取

在 ForkJoinSumCalculator 的例子中,我们决定在要求和的数组中最多包含10 000个项目
时就不再创建子任务了。这个选择是很随意的,但大多数情况下也很难找到一个好的启发式方法
来确定它,只能试几个不同的值来尝试优化它。在我们的测试案例中,我们先用了一个有1000
万项目的数组,意味着 ForkJoinSumCalculator 至少会分出1000个子任务来。这似乎有点浪费
资源,因为我们用来运行它的机器上只有四个内核。在这个特定例子中可能确实是这样,因为所
有的任务都受CPU约束,预计所花的时间也差不多。
但分出大量的小任务一般来说都是一个好的选择。这是因为,理想情况下,划分并行任务时,
应该让每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙。不幸的是,实际中,每
个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如
磁盘访问慢,或是需要和外部服务协调执行。
分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应
用中,这意味着这些任务差不多被平均分配到 ForkJoinPool 中的所有线程上。每个线程都为分
配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执
行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经
空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队
列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队
列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程
之间平衡负载。
一般来说,这种工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。当工作线程队列中有一个任务被分成两个子任务时,一个子任务就被闲置的工作线
程“偷走”了。如前所述,这个过程可以不断递归,直到规定子任务应顺序执行的条件为真。

想把多个项目合并怎么弄 java java项目分工如何合并_Fork/Join_03

现在你应该清楚流如何使用分支/合并框架来并行处理它的项目了,不过还有一点没有讲。
本节中我们分析了一个例子,你明确地指定了将数字数组拆分成多个任务的逻辑。但是,使用本
章前面讲的并行流时就用不着这么做了,这就意味着,肯定有一种自动机制来为你拆分流。这种
新的自动机制称为 Spliterator ,

public class ForkJoinPoolTest {

    private static int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    public static void main(String[] args) {
        System.out.println("result=> " + calc());
        AccumulatorRecursiveTask task = new AccumulatorRecursiveTask(0, data.length, data);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Integer result = forkJoinPool.invoke(task);
        System.out.println("AccumulatorRecursiveTask >>" + result);

        AccumulatorRecursiveAction action = new AccumulatorRecursiveAction(0, data.length, data);
        forkJoinPool.invoke(action);
        System.out.println("AccumulatorRecursiveAction >>" + AccumulatorRecursiveAction.AccumulatorHelper.getResult());
    }


    private static int calc() {
        int result = 0;
        for (int i = 0; i < data.length; i++) {
            result += data[i];
        }
        return result;
    }

}
import java.util.concurrent.RecursiveTask;

public class AccumulatorRecursiveTask extends RecursiveTask<Integer> {

    private final int start;

    private final int end;

    private final int[] data;

    private final int LIMIT = 3;

    public AccumulatorRecursiveTask(int start, int end, int[] data) {
        this.start = start;
        this.end = end;
        this.data = data;
    }


    @Override
    protected Integer compute() {
        if ((end - start) <= LIMIT) {
            int result = 0;
            for (int i = start; i < end; i++) {
                result += data[i];
            }
            return result;
        }

        int mid = (start + end) / 2;
        AccumulatorRecursiveTask left = new AccumulatorRecursiveTask(start, mid, data);
        AccumulatorRecursiveTask right = new AccumulatorRecursiveTask(mid, end, data);
        left.fork();

        Integer rightResult = right.compute();
        Integer leftResult = left.join();

        return rightResult + leftResult;
    }
}
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.atomic.AtomicInteger;

public class AccumulatorRecursiveAction extends RecursiveAction {
    private final int start;

    private final int end;

    private final int[] data;

    private final int LIMIT = 3;

    public AccumulatorRecursiveAction(int start, int end, int[] data) {
        this.start = start;
        this.end = end;
        this.data = data;
    }

    @Override
    protected void compute() {

        if ((end - start) <= LIMIT) {
            for (int i = start; i < end; i++) {
                AccumulatorHelper.accumulate(data[i]);
            }
        } else {
            int mid = (start + end) / 2;
            AccumulatorRecursiveAction left = new AccumulatorRecursiveAction(start, mid, data);
            AccumulatorRecursiveAction right = new AccumulatorRecursiveAction(mid, end, data);
            left.fork();
            right.fork();
            left.join();
            right.join();
        }
    }

    static class AccumulatorHelper {

        private static final AtomicInteger result = new AtomicInteger(0);

        static void accumulate(int value) {
            result.getAndAdd(value);
        }

        public static int getResult() {
            return result.get();
        }

        static void rest() {
            result.set(0);
        }
    }
}

本文参考:
Java–8--新特性–串并行流与ForkJoin框架线程基础:多任务处理(12)——Fork/Join框架(基本使用)Fork/Join框架基本使用 《Java8 in Action》电子版