并行数据处理与性能详解与ForkJoin框架

本章节可以让你用Stream接口不费力气就能对数据集执行并行操作,可以声明性的讲顺序流变成并行流。

一、并行流

Stream接口可以调用方法parallelStream很容易把集合转换为并行流。所谓并行流就是把内容分成多个数据块,用不同线程处理每块数据。

1、将顺序流转换成并行流

可以将流转换成并行流,调用方法parallel。例:

public static long parallelSum(long n) {
      return Stream.iterate(1L,i -> i+1)
              .limit(n)
              .parallel()
              .reduce(0L,Long::sum);
    }

如果从并行流变成顺序流可以调用sequential这个方法完成。

stream.parallel()
.filter(...)
.sequential()
.map(...)
.parallel()
.reduce();

2、测量流的性能

按常理并行求和方法应该比迭代方法性能好。然而在软件工程上,靠猜是绝对不行的,因此我们要进行实战看结果。

public class ParallelStreams {

    public static long measureSumPerf(Function<Long, Long> adder, long n) {
        long fastest = Long.MAX_VALUE;
        for (int i = 0; i < 10; i++) {
            long start = System.nanoTime();
            long sum = adder.apply(n);
            long duration = (System.nanoTime() - start) / 1_000_000;
            System.out.println("Result: " + sum);
            if (duration < fastest) fastest = duration;
        }
        return fastest;
    }

    public static long iterativeSum(long n) {
        long result = 0;
        for (long i = 1L; i <= n; i++) {
            result += i;
        }
        return result;
    }

    public static long parallelSum(long n) {
        return Stream.iterate(1L, i -> i + 1)
                .limit(n)
                .parallel()
                .reduce(0L, Long::sum);
    }

    public static long sequentialSum(long n) {
        return Stream.iterate(1L, i -> i + 1)
                .limit(n)
                .reduce(0L, Long::sum);
    }

    public static void main(String[] args) {
        System.out.println("Sequential sum done in:" +
                measureSumPerf(ParallelStreams::sequentialSum, 10_000_000) + " msecs");

        System.out.println("Iterative sum done in:" +
                measureSumPerf(ParallelStreams::iterativeSum, 10_000_000) + " msecs");

        System.out.println("Parallel sum done in: " +
                measureSumPerf(ParallelStreams::parallelSum, 10_000_000) + " msecs" );
    }
}

运行结果:
在这里插入图片描述
运行结果相当令人失望,求和方法并行是顺序版本的将近10倍,为什么会出现这样的结果,实际上有两个问题:
1、iterate生成是装箱的对象,必须拆箱才能求和。
2、很难把iterate分成多个独立块来并行执行。iterate很难分割成能独立执行的小块,因为每次应用这个函数都要 依赖前一次应用执行结果。

使用有针对性的方法,避免装箱拆箱操作和能分成独立块并行。可以使用,LongStream.rangeClosed与iterate相比有两个优点。
1、产生原始long数字,没有装箱拆箱操作。
2、会生成数字范围,很容易拆成小块。
例:

 public static long rangedSum(long n) {
        return LongStream.rangeClosed(1, n)
                .reduce(0L, Long::sum);
    }

    public static long parallelRangedSum(long n) {
        return LongStream.rangeClosed(1, n)
                .parallel()
                .reduce(0L, Long::sum);
    }
    
   System.out.println("Parallel sum done in: " +
                measureSumPerf(ParallelStreams::rangedSum, 10_000_000) + " msecs" );

   System.out.println("Parallel sum done in: " +
                measureSumPerf(ParallelStreams::parallelRangedSum, 10_000_000) + " msecs" );

执行结果:
在这里插入图片描述
得到结果终于比顺序执行快,我们使用并行流的时候一定要正确使用,比如算法改变了某些共享状态。

二、分之/合并框架ForkJoinPool

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

1、使用RecursiveTask

要把任务提交到线程池中,必须创建RecursiveTask的子类,重写compute方法。R是并行化产生的结果类型。如果任务不返回结果用RecursiveAction。
protected abstract R compute();这个方法同时定义了将任务拆分成子任务的逻辑,和任务无法在拆分时,生成单个任务逻辑。

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

用分之/合并框架并行求和

public class ForkJoinSumCalculator extends 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);
        //利用另一个ForkJoinPool线程异步执行创建子任务
        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;
    }

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

    public static void main(String[] args) {

        System.out.println(forkJoinSum(10000000));
    }
}

使用分之/合并框架需注意几点:
1、对于一个任务调用join方法会阻塞调用方,直到该任务结束。因此,在两个子任务的计算开始之后 再调用它。
2、不在RecursiveTask子类内部使用invoke方法

三、Spliterator

Spliterator是java8中加入的另一个新接口,字面意思是可分迭代器,主要作用于并行执行。

public interface Spliterator<T> {、
	//按顺序一个一个使用Spliterator元素,如果有其它元素遍历返回true
	boolean tryAdvance(Consumer<? super T> action);
	//可以把元素划分出去分给第二个Spliterator
	Spliterator<T> trySplit();
	//还剩多少元素要遍历
	long estimateSize();
	//特性
	int characteristics();
}

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

境里婆娑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值