Lambda - 数据并行化

本文总结、摘录自书籍《Java 8 函数式编程》
系列文章 GitHub地址

数据并行化

并行与并发

并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多个CPU上。如果一个程序要运行两个任务,并且只有一个CPU给他们分配了不同的时间片,那么这就是并发,而不是并行。

并行化是指缩短任务执行时间,将一个任务分解成几部分,然后并行执行。这和顺序执行的任务是一样的,区别就像更多的马来拉车,话费的时间自然减少了。实际上,和顺序执行相比,并行化执行任务时,CPU 承担的工作量更大。

数据并行化指将数据分成块,为每块数据分配单独的处理单元。当需要大量数据上执行同样的操作时,数据并行化很管用。它将问题分解成可在多块数据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而获得最终答案。

并行化流操作

并行化操作流只需改变一个方法调用。如果已经有一个Stream 对象, 调用它的parallel 方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parallelStream 就能立即获得一个拥有并行能力的流。

Stream.of(1,2,3,4).parallel().map((item) -> item + 1).forEach(
    item -> {
        System.out.println(item);
    }
);

我的输出为:

4
5
2
3

集合并行化:

List<Integer> items = Arrays.asList(new Integer[]{1,2,3,4});
        items.parallelStream().forEach(
                item -> {
                    System.out.println(item);
                }
        );

我的输出为:

3
1
4
2

并行化的限制

1)reduce 方法,为了让其在并行化时能工作正常,初值必须为组合函数的恒等值。拿恒等值和其他值做 reduce 操作时,其他值保持不变。比如,使用 reduce 操作求和,组合函数为 (acc, element) -> acc + element,则其初值必须为 0,因为任何数字加 0,值不变。

2)reduce 操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变,组合操作的顺序不重要。

3)要避免的是持有锁。流框架会在需要时,自己处理同步操作,因此程序员没有必要为自己 的数据结构加锁。如果你执意为流中要使用的数据结构加锁,比如操作的原始集合,那么有可能是自找麻烦。

性能

影响并行流性能的主要因素如下:

1)数据大小

输入数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理,再将结 果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多 时,并行化处理才有意义。

2)源数据结构

每个管道的操作都基于一些初始数据源,通常是集合。将不同的数据源分割相对容易, 这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。

3)装箱

处理基本类型比处理装箱类型要快。

4)核的数量

极端情况下,只有一个核,因此完全没必要并行化。显然,拥有的核越多,获得潜在性 能提升的幅度就越大。在实践中,核的数量不单指你的机器上有多少核,更是指运行时 你的机器能使用多少核。这也就是说同时运行的其他进程,或者线程关联性(强制线程 在某些核或 CPU 上运行)会影响性能。

5)单元处理开销

比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中 每个元素身上的时间越长,并行操作带来的性能提升越明显。

通用数据结构分组

1)性能好:

​ ArrayList、数组或 IntStream.range,这些数据结构支持随机读取,也就是说它们能轻而易举地被任意分解。

2)性能一般:

​ HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。

3)性能差:

​ 有些数据结构难于分解,比如,可能要花 O(N) 的时间复杂度来分解问题。其中包括 LinkedList,对半分解太难了。还有 Streams.iterate 和 BufferedReader.lines,它们 长度未知,因此很难预测该在哪里分解。

在讨论流中单独操作每一块的种类时,可以分成两种不同的操作:无状态的和有状态的。 无状态操作整个过程中不必维护状态,有状态操作则有维护状态所需的开销和限制。 如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括 map、 filter 和 flatMap,有状态操作包括 sorted、distinct 和 limit。

并行化数组操作

Java 8引入了一些针对数组的并行操作,这些操作被类Arrays提供。初始化数组 的使用(这里使用数组下标加2作为对应下标的值):

int[] array = new int[20];
Arrays.parallelSetAll(array, i -> i + 2);

等同于:

IntStream.range(0, array.length).parallel().forEach(i -> array[i] = i + 2);

parallelPrefix 操作擅长对时间序列数据做累加,它会更新一个数组,将每一个元素替换 为当前元素和其前驱元素的和,这里的“和”是一个宽泛的概念,它不必是加法,可以是 任意一个 BinaryOperator。

int[] array = new int[5];
Arrays.parallelSetAll(array, i -> i);
Arrays.parallelPrefix(array, Integer::sum);

输出:

0 1 3 6 10
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值