在JAVA7之前,并行处理数据非常麻烦。第一,你得明确把包含数据的数据结构分成若干份。第二,你要将每个子部分分配给一个独立的线程。第三,你要在恰当的时候对它们进行同步避免不希望的竞争条件,等待所有线程完成,最后把这些部分结果合并起来。在Java 7引入了分支/合并框架,让这些操作更稳定、更不容易出错。
分支/合并框架的目的是以递归的方式将可以并行的任务拆分为更小的任务,然后将每个子任务的结果合并起来生成整体结果。要把子任务提交到ForkJoinPool必须创建RecursiveTask<R>的子类。需要实现它唯一的抽象方法 protected abstract R compute(); 在这个方法中定义了将任务拆分成子任务的逻辑,以及无法拆分时生成单个子任务结果的逻辑。
计算1到10000000的和
/** * Desc:Fork/Join框架的目的是以递归方式将可以并行的任务拆分为更小的任务,然后将每个子任务的结果合并起来生成一个整体结果。 * 要把任务提交到ForkJoinPool必须创建RecursiveTask<T> 的一个子类 * * @author wei.zw * @since 2016年7月6日 下午9:27:56 * @version v 0.1 */ public class ForkJoinSumCalculator extends RecursiveTask<Long> { /** */ private static final long serialVersionUID = -8013303660374621470L; private final long[] numbers; private final int start; private final int end; private static final long THRESHOLD = 1000; /** * @param numbers * @param start * @param end */ public ForkJoinSumCalculator(long[] numbers, int start, int end) { super(); this.numbers = numbers; this.start = start; this.end = end; } /** * @param numbers */ public ForkJoinSumCalculator(long[] numbers) { super(); this.numbers = numbers; this.start = 0; this.end = numbers.length; } /** * @see java.util.concurrent.RecursiveTask#compute() */ @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); rightTask.fork(); Long rightResult = 0L; try { rightResult = rightTask.get(); } catch (Exception e) { } Long leftResult = leftTask.join(); return leftResult + rightResult; } /** * * @return * @author wei.zw */ private Long computeSequentially() { long sum = 0; for (int i = start; i < end; i++) { sum += numbers[i]; } return sum; } public static void main(String[] args) { long[] numbers = LongStream.rangeClosed(1, 10000000).toArray(); long start = System.currentTimeMillis(); System.out.println(new ForkJoinPool().invoke(new ForkJoinSumCalculator(numbers)) + " 耗时:" + (System.currentTimeMillis() - start)); } }
结果是:50000005000000 耗时:37
优化后的
/** * @see java.util.concurrent.RecursiveTask#compute() */ @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; }
计算结果是:50000005000000 耗时:25
使用Fork/Join框架的最佳做法:
- 对一个任务调用join方法会阻塞调用方,直到该任务作出结果。因此,又必须要在两个子任务的计算都开始之后再调用它。
- 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法,应该直接调用compute或者fork方法
- 对子任务调用fork方法可以将这个子任务排进ForkJoinPool。同时对左右两边的子任务都调用似乎很自然,但是这样做的效率比直接对其中一个调用compute方法低。这样做可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
看完了基本示例,在分析一下源码;首先看一下RecursiveTask,通过名称可以知道这是一个递归Task.源码很简单
public abstract class RecursiveTask<V> extends ForkJoinTask<V> { private static final long serialVersionUID = 5232453952276485270L; //计算结果 V result; //抽象的计算方法 protected abstract V compute(); //获取计算结果 public final V getRawResult() { return result; } //设置计算结果 protected final void setRawResult(V value) { result = value; } //执行计算 protected final boolean exec() { result = compute(); return true; } }
RecursiveTask源码看完以后,继续分析ForkJoinTask