Java并行开发——并行数据

流合并

Stream API,包括 filter、map、sorted 都统称为 聚合操作

聚合操作就是把集合中的对象做整体性的计算。

一般来说,计算操作处理 这几个词都是表达的同一个意思,都是比较宽泛的含义。尤其是计算,不要以为仅仅是加减乘除。

小案例

对 1-10 的十个正整数求和:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

 普通的使用 for 循环完成计算的代码实现是:

int sum = 0;
for (int i : numbers) {
    sum += i;
}

System.out.println("sum : " + sum);

 而使用 Stream API 完成计算的代码实现是:

import java.util.Arrays;

int sum = numbers.stream()
    .reduce((a, b) -> a + b)
    .get();

System.out.println("1-10求和 : " + sum);

reduce() 方法的作用,是合并了所有的元素,终止 计算出一个结果。注意这里终止的意思,就是流已经到达终点结束了,不能再继续流动了。

reduce() 方法的返回值是一个比较复杂的对象,需要调用 get() 方法返回最终的整数值。

同理,get() 方法返回值的类型,也是系统自动根据流中元素类型推定的。

reduce() 方法的参数就稍微有点复杂了(重点理解):

  • a 在第一次执行计算语句 a + b 时,指代流的第一个元素;然后充当缓存作用以存放本次计算结果。此后执行计算语句时,a 的值就是上一次的计算结果并继续充当缓存存放本次计算结果。
  • b 参数第一次执行计算语句时指代流的第二个元素。此后依次指代流的每个元素。

注意:a、b 两个参数的作用是由位置决定的,变量名是任意的

结合下图理解:

reduce() 方法的第一个参数(本例的 a)有多重作用,并且系统是自动完成参数(本例的 a, b)赋值的,所以仍然体现了 Stream 编程的重点仍然是计算(本例的 a + b)。 

reduce() 方法也是可以操作对象的。

对于上节习题中的数据:

List<Student> students = new ArrayList<>();
students.add(new Student("赵祯", 92));
students.add(new Student("曹丹姝", 60));
... ...
... ...

计算三年二班的学生的分数,并在 Console 中打印本班级学生平均分。

如果需求比较复杂,可能简单的整数就不够了,就要使用 Student 对象:

Student result = students.stream()
    .reduce(
        (a, b) -> {
            a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
            return a;
        }
    )
    .get();

System.out.println(result.getName() + " - " + result.getMidtermScore());

Console 的输出结果是:

  赵祯 - 777

这是 bug,如果计算后,再在网页上显示每位同学的分数,赵祯同学的分数就错误的显示成 777 分。

出现 bug 的主要原因是,第一个 Student 对象由于充当了缓存角色,正确性被破坏了。

解决办法

reduce() 提供了另一种参数形式,可以自己 new 一个对象充当缓存角色,而不是使用流中的原始对象。

Student result = students.stream()
    .reduce(new Student("", 0),
        (a, b) -> {
            a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
            return a;
        }
    );

System.out.println(result.getName() + " - " + result.getMidtermScore());

reduce() 方法的参数变为了两个:

  • 第一个参数,是作为缓存角色的对象
  • 第二个参数,是 Lambda 表达式,完成计算,格式是一样的。
    • 那么 a 变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。
    • b 变量依次指代流的每个元素,包括第一个元素。
    • ab 职责非常清晰了。

对照下图理解 ab 参数的功能变化:

reduce() 方法的返回值同样发送了变化,返回作为缓存角色的对象,即第一个参数。

不用再调用一次 get() 方法了。

  赵祯 - 92
   - 777

流中的原始对象没有被破坏,在一个没有姓名的缓存对象中存放计算结果。没有 bug 了

小结

从 reduce() 的学习可以感受到,对于整个 Java  的设计,由于系统会自动做很多事情,所以在学习和理解时,知识点是比较隐晦的,逻辑也有点复杂。

但是一旦理解后,写代码就爽快很多,编程的焦点更加明确了。

可以说,特点是 “约定大于代码” 。Java 把规范制定好了,开发者只要专注于开发计算逻辑。

流收集

在实际工作中,整体功能如果比较复杂的话,使用流对集合进行计算后,可能并不想输出和合并,而是把结果元素放在一个新的集合中,待进一步使用。

例如,新的集合可以传递给 Thymeleaf 模板等等。

小习题:对于一组数字:

List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);

找出最大的前 3 个数字放入一个新的集合中,用 - 组合成字符串打印。

import java.util.stream.Collectors;

List<String> numResult = numbers.stream()
    .sorted((n1, n2) -> n2 - n1)
    .limit(3)
    .map(a -> "" + a)
    .collect(Collectors.toList());

String string = String.join("-", numResult);
System.out.println("字符串是: " + string);

collect() 方法的作用就是收集元素,但元素收集存放到哪去呢?Collectors.toList() 是一个静态方法,作为参数告诉 collect() 方法存入一个 List 集合。所以 collect() 方法的返回值类型就是 List

java.util.stream.Collectors 是流工具包中提供的收集器。

为了能够把最终结果转换为字符串打印,调用了 map() 方法把流中原来的整数映射为字符串("" + a),所以 collect() 方法的返回值类型就是 List<String>,而不是 List<Integer>

String.join() 的用法已经在《Java面向对象》课程中学过了

collect(Collectors.toList()) 几乎是一个标准用法了。

并行流

我们回忆一下 Stream API 的设计,很像是一个管道:

管道的显著特点是,每个节点是依次执行的,下一个节点必须等待上一个节点执行完毕。这种执行方式,通常叫作 串行

无论多少个节点都排成一个队伍

性能问题

如果计算过程越来越复杂、数据量越来越大,串行工作模式性能会越来越低。我们学习过程中所举的案例,数据量没那么大,这是为了便于理解。由于目前已经进入大数据时代,在实际工作中的数据,可能会数据量大、计算过程复杂。

串行工作模式的性能很难被优化。因为这种模式无法发挥 多核 CPU 的优势。

只有一个队伍,核再多也只能等待

解决办法

为了充分发挥 多核 CPU 的优势,可以把 串行 计算模式,改为 并行 计算模式。

当然,并行 计算模式不能简单的把一个队伍变成三五个队伍。

所谓并行,就是利用多线程,变成同时执行。多线程可以充分发掘多核 CPU 的优势。

小案例

对 500000~1 中的所有偶数,求平方值后进行自然排序。比较一下 串行 和 并行 的性能差异。

使用并行流的代码很简单,不再调用 stream() 方法,改为调用 parallelStream() 方法即可。其它的计算方法是一样的。

parallelStream() 以并行的方式执行任务,同时也支持流的收集、合并等计算。结合下图理解与串行运算的不同:

同样。并行流的计算过程被 Java 封装了,使用起来很简单。目前需要熟练应用即可。当然,如果任务拆分等过程和细节有兴趣,可以研究 Stream API 源码。

大家多运行几次,会看到每次耗时毫秒数都不完全相同,总体来说,还是 并行 计算模式耗时更短。

我们在《Java面向对象》第 8 章学习过使用 Duration.between() 方法计算两个时间的差值哦。duration.toMillis() 输出毫秒数

不适合使用并行计算的场景

流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算。

上面视频中的例子可以看到,数字输出的顺序不是期望中的 1 2 3 4 5,而是无序的,而且多运行几次,顺序都可能不一样。

这是因为并行计算使用了多线程,每个线程独立输出数字,而线程的输出时机,是由 CPU 动态决定的,无法确定。

所以,逻辑上要求数字必须按书写的前后顺序(数字之间有逻辑顺序)输出时,就不能使用并行计算。

 并行流的性能意外

实际上,并行 计算模式的性能 不是 任何情况下都优于 串行 计算模式。

场景的原因有两种:

一、硬件太差

CPU 核数很低,特别是单核情况下,并行 计算模式不一定更好。因为多线程也要等待 CPU 资源,不能很好的发挥多线程的优势。

二、任务简单

数据量小、任务简单的情况下,并行 计算模式不一定更好。因为多线程的管理也是会消耗CPU内存等资源的,可能比任务本身的开销更大。

选择

我们必须先理解 串行 与 并行 两种模式的差别,然后根据实际情况选择使用。

由于实际情况中,硬件、需求复杂度等各种因素比较复杂,所以实际上没有确定的选择方案。

一般来说,任务执行超过一小时的情况下,考虑使用 并行 模式优化性能。

任务执行时间较短,又没有特别要求,使用 串行 模式也问题不大。

对任务有实时性要求,希望立即得到计算结果,最好是比较一下两种模式,比如各运行 100 次比较使用哪种模式最好。

使用 并行 要特别注意是否会造成数据之间是否有逻辑关系,出 bug 就不好了。

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Java 8中新增的Stream是一种处理集合的优雅姿势。 Stream是对集合(Collection)对象功能的增强,它能以一种声明的方式来处理数据,实现类似于SQL语句的操作。Stream不会改变原有的数据结构,它会生成一个新的Stream,同时支持并行化操作。 Stream的核心思想是将数据看作是,而上可以进行各种操作,比如过滤、排序、映射等。这样可以将数据处理过程变得非常简洁和灵活。 下面是一些Stream的常用操作: 1. filter:过滤符合条件的元素 ``` List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); list.stream().filter(i -> i % 2 == 0).forEach(System.out::println); //输出2, 4 ``` 2. map:将元素转换成另一种类型 ``` List<String> list = Arrays.asList("apple", "banana", "orange"); list.stream().map(s -> s.toUpperCase()).forEach(System.out::println); //输出APPLE, BANANA, ORANGE ``` 3. sorted:对元素进行排序 ``` List<Integer> list = Arrays.asList(5, 2, 1, 4, 3); list.stream().sorted().forEach(System.out::println); //输出1, 2, 3, 4, 5 ``` 4. distinct:去重 ``` List<Integer> list = Arrays.asList(1, 2, 3, 2, 1); list.stream().distinct().forEach(System.out::println); //输出1, 2, 3 ``` 5. limit:限制元素个数 ``` List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); list.stream().limit(3).forEach(System.out::println); //输出1, 2, 3 ``` 6. skip:跳过元素 ``` List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); list.stream().skip(2).forEach(System.out::println); //输出3, 4, 5 ``` 7. reduce:对元素进行聚合操作 ``` List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); int sum = list.stream().reduce(0, (a, b) -> a + b); System.out.println(sum); //输出15 ``` Stream的操作可以组合起来形成一个水线,每个操作都会返回一个新的Stream对象,这样就可以形成一个操作序列。最后调用终止操作(如forEach、findAny等)才会触发所有中间操作的执行。 使用Stream处理集合的代码通常比使用传统的循环更简洁,同时也更易于并行化处理,提高了程序的效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

DF_Orange

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

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

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

打赏作者

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

抵扣说明:

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

余额充值