从入门到入土:java流式编程

Java 流式编程

1. 基础知识

1.1 流式编程简介

流式编程(Stream Processing)是一种编程范式,它强调数据的 连续流动逐步处理,避免显式的迭代控制,使代码更 简洁、声明式、易并行


1. 什么是流(Stream)?

流(Stream) 是一种 数据序列,它支持一系列 高效、声明式的操作,用于 转换、过滤、聚合 等任务。例如,在 Java 的 Stream API 或 Python 的 itertoolspandas 中,流都能 懒加载数据 并逐步处理,避免一次性加载整个数据集,提高效率。

流的特性:

  • 单向传输:数据从 源头 经过 一系列处理,最终形成 结果,不会回溯。
  • 懒执行:只有 最终需要数据 时(如 collect()toList()),流才会真正执行计算,提高性能。
  • 不可复用:流 一旦被消费,就不能重复使用,但可以生成新的流。
  • 支持并行:流式处理可以 并行化,充分利用多核 CPU 提高吞吐量。

示例(Java Stream API):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filteredNames = names.stream() // 生成流
    .filter(name -> name.startsWith("A")) // 过滤
    .map(String::toUpperCase) // 转换
    .collect(Collectors.toList()); // 终止操作,收集结果

System.out.println(filteredNames); // 输出 [ALICE]

这段代码:

  1. 创建流names.stream()
  2. 过滤 → 仅保留以 “A” 开头的元素
  3. 映射 → 将所有保留的元素转为大写
  4. 终结操作collect() 触发计算并生成新列表

相比传统 for 循环,流代码更 简洁、声明式、并行友好


2. 流式编程的优势

流式编程的核心优势主要体现在 简洁性、声明式风格、并行处理 三个方面:

1. 简洁

  • 避免显式循环,减少 forwhile 代码,使逻辑更清晰。
  • 链式调用,一条语句完成多个数据操作,减少样板代码。

示例(传统循环 vs. 流式编程):

// 传统方式:手动管理循环逻辑
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.toUpperCase());
    }
}

// 流式编程:链式调用 + 直接表达“想要的结果”
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

2. 声明式

  • 传统编程强调 “怎么做”(如循环、索引管理),流式编程强调 “要什么”(如过滤、映射、聚合)。
  • 代码更可读、更易维护,类似 SQL 查询风格。

3. 并行处理

  • 流可 并行执行parallelStream()),自动拆分数据块,充分利用 多核 CPU 进行计算。
  • 传统循环是 顺序执行,无法轻松实现并行化。

示例(并行流):

List<String> parallelResult = names.parallelStream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

这里 parallelStream() 使流 并行执行,在大数据集时可显著提高性能。


3. 流(Stream)与集合(Collection)的区别
特性流(Stream)集合(Collection)
存储不存储数据,只负责数据传输和处理存储具体数据元素
迭代一次性迭代,消费后不可复用可反复遍历
惰性计算只有 终结操作 触发时才执行立即计算,存储计算结果
可变性不可变,每次操作生成新流可变,可以添加/删除元素
并行处理天然支持并行(parallelStream)默认顺序处理,需手动实现并行
计算效率更高(只处理必要数据)可能较低(存储整个数据集)

简单理解:

  • 流(Stream) = 快递传输(数据在流动,快递员送达后不回头)
  • 集合(Collection) = 仓库存储(数据存放在一个集合,可以随时取出)

示例:

List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream(); // 生成流

stream.forEach(System.out::println); // 只能消费一次
stream.forEach(System.out::println); // 运行时报错(流已消费)

但是,集合则可以反复使用:

System.out.println(list); // ["A", "B", "C"]
System.out.println(list); // 仍然可用

总结

  • 流(Stream) 适用于 高效处理数据,尤其是 大数据、需要懒加载、并行计算的场景
  • 集合(Collection) 适用于 存储和反复操作数据,更适合小规模数据存储和管理。

4. 何时使用流(Stream)?
  • 需要 对数据进行转换、过滤、聚合 时(如 map()filter()reduce())。
  • 处理 大规模数据 时,避免一次性加载整个集合,提高性能。
  • 需要 并行处理 时,流可以自动优化计算任务。

流式编程适用于 高效数据处理,但不适合需要反复访问数据的场景。在开发中,根据 计算需求 选择 流(Stream)集合(Collection),可以大大提升代码的 可读性执行效率


结论
  1. 流(Stream)是一种数据处理方式,不是数据存储容器
  2. 流式编程比传统循环更简洁、更声明式、更支持并行处理
  3. 流(Stream)一次性迭代,集合(Collection)可重复使用
  4. 流式编程适用于数据转换、过滤、并行计算等场景,而 集合适用于数据存储和管理

1.2 流的创建

在 Java 中,流(Stream) 不是数据结构,而是数据的 管道,用于 高效、声明式 处理数据。流的创建有多种方式,主要来源包括 集合、数组、静态工厂方法、I/O 读取 等。


1. 从集合创建流

集合(Collection)是最常见的流来源,主要通过 stream()parallelStream() 方法。

List<String> list = Arrays.asList("Alice", "Bob", "Charlie");

// 1. 顺序流(默认)
Stream<String> stream = list.stream();
stream.forEach(System.out::println);

// 2. 并行流(多线程处理)
Stream<String> parallelStream = list.parallelStream();
parallelStream.forEach(System.out::println);
stream() vs parallelStream()
方法作用
stream()顺序流,按 原始顺序 逐个处理数据
parallelStream()并行流自动拆分任务 并分配给 多个 CPU 线程,适合 大数据处理

示例(顺序 vs. 并行)

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 顺序流(单线程)
names.stream()
    .forEach(name -> System.out.println(Thread.currentThread().getName() + " -> " + name));

// 并行流(多线程)
names.parallelStream()
    .forEach(name -> System.out.println(Thread.currentThread().getName() + " -> " + name));

输出示例(并行流可能不同)

main -> Alice
main -> Bob
main -> Charlie  // 顺序流,都是 main 线程

ForkJoinPool.commonPool-worker-1 -> Alice
ForkJoinPool.commonPool-worker-2 -> Bob
ForkJoinPool.commonPool-worker-3 -> Charlie  // 并行流,多个线程

适用场景:

  • 小数据量(< 1 万条) → stream()
  • 大数据量(> 1 万条) → parallelStream()
  • 涉及 I/O 操作 → 避免 parallelStream()(线程争用 I/O 资源)

2. 从数组创建流

数组不能直接调用 stream() 方法,但可以使用 Arrays.stream()Stream.of() 来创建流。

String[] array = {"Apple", "Banana", "Cherry"};

// 1. Arrays.stream()
Stream<String> stream1 = Arrays.stream(array);
stream1.forEach(System.out::println);

// 2. Stream.of()
Stream<String> stream2 = Stream.of(array);
stream2.forEach(System.out::println);

区别:

  • Arrays.stream(array) 适用于 完整数组或部分子数组
  • Stream.of(array) 适用于 整个数组

子数组流

int[] numbers = {1, 2, 3, 4, 5};
IntStream subStream = Arrays.stream(numbers, 1, 4); // 只取索引 1~3
subStream.forEach(System.out::println);  // 输出 2, 3, 4

3. 使用静态工厂方法创建流
3.1 Stream.of()

Stream.of() 直接创建流,适用于少量数据:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.forEach(System.out::println);
3.2 Stream.iterate()

iterate() 生成 无限流,适用于生成序列数据(必须加 limit() 限制长度)。

Stream<Integer> stream = Stream.iterate(1, n -> n + 2).limit(5);
stream.forEach(System.out::println);  // 输出 1, 3, 5, 7, 9

解释:

  • 1 → 起始值
  • n -> n + 2 → 递增规则
  • limit(5) → 限制元素个数(避免无限循环)

生成斐波那契数列

Stream.iterate(new int[]{0, 1}, arr -> new int[]{arr[1], arr[0] + arr[1]})
    .limit(10)
    .map(arr -> arr[0])  // 取出每个数组的第一个值
    .forEach(System.out::println);
3.3 Stream.generate()

generate() 也是创建 无限流,但它 不会根据前一个值生成新值,而是 每次独立生成

Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5);
randomNumbers.forEach(System.out::println);

示例:生成 UUID

Stream<String> uuidStream = Stream.generate(UUID::randomUUID).limit(3);
uuidStream.forEach(System.out::println);

iterate() vs generate()

方法特点
Stream.iterate()依赖前一个元素生成下一个,适用于数学序列
Stream.generate()每个元素独立计算,适用于随机数据

4. 其他来源(I/O 读取)
4.1 从文件创建流(Files.lines()

Files.lines() 读取文件的 每一行 并作为 Stream<String>

try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}
  • 懒加载Files.lines() 逐行读取,不会一次性加载整个文件,适合大文件处理。
  • 自动关闭:使用 try-with-resources 确保 Stream 关闭,避免内存泄漏。

4.2 从 BufferedReader 创建流

BufferedReader.lines() 读取输入流,每行转换为 Stream<String>

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    Stream<String> lineStream = reader.lines();
    lineStream.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}
  • BufferedReader 适用于小文件
  • Files.lines() 适用于大文件(更高效,支持并行)。

小结

方式方法适用场景
集合Collection.stream()适合 List、Set
并行流Collection.parallelStream()适合大数据量的集合
数组Arrays.stream(array)适合数组
静态方法Stream.of(…)少量已知元素
数列流Stream.iterate(seed, f).limit(n)适合数学递推
随机流Stream.generate(Supplier).limit(n)适合随机数据
文件Files.lines(path)读取大文件
字符流BufferedReader.lines()读取小文本文件

2 流的操作分类

在 Java 流(Stream) 中,所有的操作可以分为 两大类

  1. 中间操作(Intermediate Operations)返回新的流,支持链式调用懒执行
  2. 终端操作(Terminal Operations)触发流的执行,并结束流的生命周期

2.1 中间操作(Intermediate Operations)

特点:

  • 不会立即执行,只有在终端操作(如 collect()forEach())触发时才执行(懒执行)。
  • 返回新的流,支持链式调用
2.1.1 filter(Predicate<T>) - 过滤

filter() 根据 条件 过滤流中的元素,保留符合条件的元素

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 过滤出以 "A" 开头的名字
List<String> filteredNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(filteredNames); // 输出: [Alice]

2.1.2 map(Function<T, R>) - 映射

map() 转换流中的每个元素,返回一个新的流。例如:

  • 转换类型(StringInteger)。
  • 修改数据格式("Alice""ALICE")。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 将名字转换为大写
List<String> upperCaseNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(upperCaseNames); // 输出: [ALICE, BOB, CHARLIE]

示例 2:计算平方值

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

List<Integer> squares = numbers.stream()
    .map(n -> n * n)  // 每个元素平方
    .collect(Collectors.toList());

System.out.println(squares); // 输出: [1, 4, 9, 16, 25]

2.1.3 flatMap(Function<T, Stream<R>>) - 扁平化

flatMap() 适用于 嵌套结构(如 List<List<String>>),将其打平为一个流。

List<List<String>> nestedList = Arrays.asList(
    Arrays.asList("Apple", "Banana"),
    Arrays.asList("Cherry", "Date")
);

// 扁平化成一个单独的流
List<String> flatList = nestedList.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());

System.out.println(flatList); // 输出: [Apple, Banana, Cherry, Date]

2.1.4 sorted(Comparator<T>) - 排序

sorted() 对流中的元素进行 排序

  • 默认排序Comparable 接口)。
  • 自定义排序Comparator)。
List<Integer> numbers = Arrays.asList(5, 1, 3, 2, 4);

// 默认升序排序
List<Integer> sortedList = numbers.stream()
    .sorted()
    .collect(Collectors.toList());

System.out.println(sortedList); // 输出: [1, 2, 3, 4, 5]

自定义排序(按字符串长度排序)

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

List<String> sortedByLength = names.stream()
    .sorted((s1, s2) -> s1.length() - s2.length())
    .collect(Collectors.toList());

System.out.println(sortedByLength); // 输出: [Bob, Alice, Charlie]

2.1.5 distinct() - 去重

distinct() 去除重复元素(基于 equals() 判断)。

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

List<Integer> uniqueNumbers = numbers.stream()
    .distinct()
    .collect(Collectors.toList());

System.out.println(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]

2.1.6 limit(n)skip(n) - 截取
  • limit(n) 保留前 n 个元素
  • skip(n) 跳过前 n 个元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 只取前 3 个元素
List<Integer> limited = numbers.stream().limit(3).collect(Collectors.toList());
System.out.println(limited); // 输出: [1, 2, 3]

// 跳过前 2 个元素
List<Integer> skipped = numbers.stream().skip(2).collect(Collectors.toList());
System.out.println(skipped); // 输出: [3, 4, 5]

2.2 终端操作(Terminal Operations)

终端操作(Terminal Operations)是 Java 流式编程中非常重要的一部分,它们会触发流的执行并产生最终结果。终端操作执行后,流就会被关闭,无法再复用。以下是终端操作的详细说明和常见示例:


2.2.1 终端操作的特点
  1. 触发流的执行

    • 终端操作是流的“终点”,只有在调用终端操作时,流的中间操作才会真正执行。
    • 如果没有终端操作,中间操作不会生效(因为流是惰性求值的)。
  2. 流关闭

    • 终端操作执行后,流会被关闭,无法再使用。如果尝试复用已关闭的流,会抛出 IllegalStateException
  3. 返回非流类型

    • 终端操作返回的结果通常是非流类型,例如集合、基本类型、Optional<T> 等。

2.2.2 常见的终端操作
1. forEach()
  • 功能:对流中的每个元素执行操作。
  • 返回值void
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.stream()
           .forEach(System.out::println); // 打印每个元素
    

2. collect()
  • 功能:将流中的元素收集到一个容器中(如列表、集合、映射等)。
  • 返回值:一个容器(如 ListSetMap 等)。
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> evenNumbers = numbers.stream()
                                       .filter(num -> num % 2 == 0)
                                       .collect(Collectors.toList()); // 收集到 List 中
    System.out.println(evenNumbers); // 输出: [2, 4]
    

3. reduce()
  • 功能:将流中的元素组合成一个单一的结果。
  • 返回值Optional<T> 或具体值(取决于是否有初始值)。
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    int sum = numbers.stream()
                     .reduce(0, (a, b) -> a + b); // 求和
    System.out.println(sum); // 输出: 15
    

4. count()
  • 功能:统计流中的元素数量。
  • 返回值long
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    long count = numbers.stream()
                        .filter(num -> num % 2 == 0)
                        .count(); // 统计偶数数量
    System.out.println(count); // 输出: 2
    

5. min() / max()
  • 功能:查找流中的最小或最大元素。
  • 返回值Optional<T>
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> min = numbers.stream()
                                   .min(Integer::compareTo); // 查找最小值
    min.ifPresent(System.out::println); // 输出: 1
    

6. anyMatch() / allMatch() / noneMatch()
  • 功能:检查流中的元素是否满足某个条件。
    • anyMatch():至少有一个元素满足条件。
    • allMatch():所有元素都满足条件。
    • noneMatch():没有元素满足条件。
  • 返回值boolean
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    boolean hasEven = numbers.stream()
                             .anyMatch(num -> num % 2 == 0); // 是否有偶数
    System.out.println(hasEven); // 输出: true
    

7. findFirst() / findAny()
  • 功能:查找流中的第一个或任意一个元素。
  • 返回值Optional<T>
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> firstEven = numbers.stream()
                                         .filter(num -> num % 2 == 0)
                                         .findFirst(); // 查找第一个偶数
    firstEven.ifPresent(System.out::println); // 输出: 2
    

8. toArray()
  • 功能:将流中的元素转换为数组。
  • 返回值Object[] 或指定类型的数组。
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Integer[] array = numbers.stream()
                             .toArray(Integer[]::new); // 转换为 Integer 数组
    System.out.println(Arrays.toString(array)); // 输出: [1, 2, 3, 4, 5]
    

9. forEachOrdered()
  • 功能:对流中的每个元素执行操作,保证顺序(尤其在并行流中)。
  • 返回值void
  • 示例
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.parallelStream()
           .forEachOrdered(System.out::println); // 保证顺序打印
    

2.2.3 终端操作的注意事项
  1. 流的关闭

    • 终端操作执行后,流会被关闭,无法再使用。如果需要重新处理数据,必须重新创建流。
  2. 短路操作

    • 某些终端操作(如 findFirst()anyMatch())是短路操作,一旦找到结果就会立即停止流的处理。
  3. 并行流的线程安全

    • 在并行流中使用终端操作时,确保操作是线程安全的,避免副作用。

2.2.4 小结
终端操作功能返回值示例
forEach()对每个元素执行操作voidstream.forEach(System.out::println)
collect()收集元素到容器容器(如 ListSetstream.collect(Collectors.toList())
reduce()组合元素Optional<T> 或具体值stream.reduce(0, (a, b) -> a + b)
count()统计元素数量longstream.filter(...).count()
min() / max()查找最小/最大值Optional<T>stream.min(Integer::compareTo)
anyMatch()检查是否有元素满足条件booleanstream.anyMatch(num -> num % 2 == 0)
findFirst()查找第一个元素Optional<T>stream.findFirst()
toArray()转换为数组Object[] 或指定类型数组stream.toArray(Integer[]::new)
forEachOrdered()保证顺序执行操作voidstream.forEachOrdered(System.out::println)

3. 高级操作

3.1 并行流(Parallel Stream)

并行流(Parallel Stream)是 Java Stream API 提供的一种 并行处理数据 的机制,它能自动拆分数据并分配给多个 CPU 线程,充分利用多核 CPU 提高程序执行效率。


3.2 并行流 vs. 顺序流

特性顺序流(Serial Stream)并行流(Parallel Stream)
执行方式单线程 顺序处理多线程并行 执行
数据处理按照数据的原始顺序处理拆分数据块,并发处理
适用场景小数据量需保持顺序大数据量 处理
底层实现单线程 stream()Fork/Join 框架(多线程)

示例:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 顺序流(单线程)
names.stream().forEach(name ->
    System.out.println(Thread.currentThread().getName() + " -> " + name));

// 并行流(多线程)
names.parallelStream().forEach(name ->
    System.out.println(Thread.currentThread().getName() + " -> " + name));

可能的输出(并行流的执行顺序不固定)

ForkJoinPool.commonPool-worker-1 -> Alice
ForkJoinPool.commonPool-worker-2 -> Bob
ForkJoinPool.commonPool-worker-3 -> Charlie
ForkJoinPool.commonPool-worker-4 -> David

3.3 如何创建并行流?

Java 提供 两种方式 创建并行流:

3.3.1 parallelStream() - 直接创建并行流

适用于 集合类型ListSetMap)。

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

// 直接创建并行流
numbers.parallelStream()
    .forEach(n -> System.out.println(Thread.currentThread().getName() + " -> " + n));

3.3.2 stream().parallel() - 转换顺序流为并行流

适用于 已有的顺序流,可以动态切换为并行流

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

numbers.stream()
    .parallel() // 转换为并行流
    .forEach(n -> System.out.println(Thread.currentThread().getName() + " -> " + n));

两者的区别

方式适用对象说明
parallelStream()集合(List、Set、Map)直接创建并行流
stream().parallel()已有顺序流需要 动态转换 为并行流

并行流是 Java 8 引入的一个非常强大的特性,它能够让你在处理大量数据时,利用多核 CPU 的优势来加速计算。尽管它带来了性能上的潜力,但并行流的使用也有一些复杂性和注意事项,理解这些原理与优化技巧至关重要。下面详细展开 并行流的性能优化与注意事项,包括其底层原理、优化技巧以及常见的陷阱。

3.4 并行流的底层原理

并行流底层是依赖于 ForkJoinPool 框架进行并行计算的。ForkJoinPool 是一个为并行任务设计的线程池,能够动态地分配线程并通过工作窃取(work-stealing)机制来提高性能。并行流在内部使用 ForkJoinPool 来并行处理任务。以下是一些关键点:

  • 线程池大小: 默认情况下,ForkJoinPool 的并行度(即并行计算的线程数)是 CPU 核心数 - 1。例如,如果你的机器有 8 个 CPU 核心,默认的并行度是 7。你可以通过如下代码查看默认的并行度:

    System.out.println(ForkJoinPool.commonPool().getParallelism()); 
    // 输出:默认并行线程数,通常是 CPU 核心数 - 1
    
  • 数据拆分: 并行流会自动将数据拆分成多个小块,分配给多个线程执行。这些线程会并行处理这些小块的数据,最后将结果合并。

    • 比如在处理一个大集合时,Stream.parallel() 会自动将集合分成多个子集,并将每个子集分配给不同的线程。每个线程处理自己的子集,完成后再合并结果。
  • 并行计算机制: ForkJoinPool 使用一种叫做“工作窃取”的机制(work-stealing),这意味着如果某个线程完成任务较早,它可以从其他线程中窃取任务来执行,以确保负载均衡。

注意: 虽然并行流有很大的性能优势,但并不是所有的任务都适合使用并行流。对于一些小数据量或计算非常简单的任务,使用并行流反而可能引入额外的开销,导致性能下降。

3.5 并行流的优化技巧

并行流的使用并不仅仅是启用 .parallel() 那么简单。为了从并行流中获得最大的性能提升,下面是一些优化技巧和建议:

  1. 适用于大数据集:

    • 并行流的性能提升在数据量较大的情况下尤为明显。通常建议使用并行流处理 10,000+ 条数据,当数据量较小时,线程的创建与管理开销可能超过并行处理的好处。

    • 对于小数据集,顺序流stream())反而可能表现更好,因为创建线程的开销较大,而且线程之间的上下文切换也会消耗时间。

  2. 避免修改共享变量:

    • 并行流的一个重要规则是避免在多个线程之间共享可变状态,尤其是在操作中进行写操作时。并行流中的 forEach() 操作是 无序的,因此如果你在并行流中修改共享变量,可能会引发 并发问题,导致结果不一致。

    • 线程安全的并行操作: 为了保证并行流的正确性,应尽量避免修改外部状态。比如,避免在 forEach() 内部对外部变量进行累加或修改。如果确实需要修改共享变量,建议使用 AtomicInteger 或者通过 synchronized 关键字来保证线程安全。

    • 例子:

      // 错误示范:在并行流中修改共享变量
      int[] sum = {0};  // 共享变量
      Arrays.stream(data).parallel().forEach(i -> sum[0] += i);  // 可能发生并发问题
      
      // 正确做法:避免修改共享变量
      AtomicInteger sum = new AtomicInteger(0);  // 使用线程安全的 AtomicInteger
      Arrays.stream(data).parallel().forEach(i -> sum.addAndGet(i));  // 安全的并行操作
      
  3. 优先使用 collect() 而非 forEach()

    • 在处理并行流时,尽量避免使用 forEach() 操作,尤其是在需要对结果进行收集时。forEach() 并没有顺序收集的保证,这可能会引发数据丢失或顺序不一致的问题。

    • collect() 是一个很好的替代,能够保证顺序收集并且避免并发问题。collect() 操作通常会使用一个线程安全的集合(如 ConcurrentLinkedQueueCollectors.toList())来汇总结果。

    • 例如,使用 collect() 可以避免不必要的并发问题:

      // 使用 collect() 来收集结果
      List<Integer> result = Arrays.stream(data)
                                   .parallel()
                                   .filter(x -> x > 10)
                                   .collect(Collectors.toList());  // 使用线程安全的收集操作
      
      // 避免直接使用 forEach() 进行收集
      Arrays.stream(data)
            .parallel()
            .forEach(x -> result.add(x));  // 可能引发并发问题
      
  4. 确保数据的“分割”是均匀的:

    • 并行流通过 ForkJoinPool 来自动分割数据。如果数据拆分不均匀,可能导致部分线程的任务量较少,而其他线程任务量过大,从而引发性能瓶颈。因此,确保数据的分割均匀至关重要。

    • 比如,对于一个 排序操作,需要确保输入数据已经是可以被合理分割的,否则会浪费线程的计算能力。对于排序操作,Java 内部实现了优化策略,能够确保足够均匀的分割。

  5. 避免不必要的操作

    • 在使用并行流时,某些操作可能导致 线程间竞争 或者 额外的同步开销,这些开销会抵消并行化带来的性能提升。特别是一些 非线程安全 的操作,比如操作共享状态、集合的排序等,尽量避免。

    • 如果并行流的任务本身就是 CPU 密集型的,合理评估是否可以通过 优化算法 或者 预处理 来减少计算量。

3.3 小结

并行流是 Java 8 引入的一个强大的工具,能够充分利用多核 CPU 的优势,提高大数据集的处理效率。然而,在使用并行流时,你需要谨慎处理共享变量、选择合适的操作(如 collect() 而不是 forEach())、确保数据拆分均匀等。

  • 对于大数据集(如 10,000+ 条数据),并行流能够提供显著的性能提升。
  • 对于小数据集,或者数据处理较简单的情况,使用并行流可能反而带来额外的开销,降低性能。
  • 保证线程安全,避免在并行流中修改共享变量。

通过这些优化技巧,你可以更好地利用并行流的特性,实现更高效的程序设计。


4. 并行流的适用场景与限制

4.1 适用场景

适用场景说明
大数据量(> 10,000 条)数据量越大,并行处理越有优势
计算密集型任务计算任务多,IO 操作少(如数学计算)
无状态操作(map()filter()不依赖外部变量,可独立计算

示例:计算大数据集的平方和

List<Integer> numbers = IntStream.range(1, 1000000).boxed().collect(Collectors.toList());

long sum = numbers.parallelStream()
    .mapToLong(n -> n * n)
    .sum();

System.out.println(sum);

4.2 不适用场景

不适用场景说明
数据量小(< 10,000 条)线程切换成本高,反而变慢
需要维护顺序parallelStream() 不能保证执行顺序
有共享资源(如 System.out.println()可能会产生 数据竞争
依赖外部变量可能引发 线程安全问题

示例:并行流导致数据竞争

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};

numbers.parallelStream().forEach(n -> sum[0] += n); // 可能出现错误的结果

System.out.println(sum[0]); // 结果可能不是 15

解决方案:使用 reduce()**

int sum = numbers.parallelStream()
    .reduce(0, Integer::sum);

System.out.println(sum); // 正确输出: 15

4.3 并行流的优缺点

优点缺点
自动利用多核 CPU,提高性能数据量小会增加线程管理开销
代码简洁,不需要手写多线程默认线程池大小固定(N - 1),可能需要手动调整
适用于计算密集型任务会影响顺序(forEachOrdered() 可解决)

4.4 何时使用并行流?

✅ 适合:

  • 大数据量(1 万条以上)。
  • 计算密集型任务(如数学计算)。
  • 数据独立,无需顺序(如 map()filter())。

避免

  • 数据量小(线程开销大于收益)。
  • 依赖顺序执行(建议用 forEachOrdered())。
  • 有共享变量(容易引发并发问题)。

4.5 小结

  1. 并行流可以自动利用多核 CPU 提高性能,但不是万能的,适用于 大数据量 + 计算密集型任务
  2. 创建方式
    • parallelStream() 直接创建并行流
    • stream().parallel() 转换顺序流为并行流
  3. 优化建议
    • 避免修改共享变量(使用 collect()reduce())。
    • 数据量小 不建议使用(线程切换成本高)。
    • 有序操作 需使用 forEachOrdered() 保证顺序。
  4. 适用于计算密集型任务,避免用于 IO 操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值