Java 流式编程
1. 基础知识
1.1 流式编程简介
流式编程(Stream Processing)是一种编程范式,它强调数据的 连续流动 和 逐步处理,避免显式的迭代控制,使代码更 简洁、声明式、易并行。
1. 什么是流(Stream)?
流(Stream) 是一种 数据序列,它支持一系列 高效、声明式的操作,用于 转换、过滤、聚合 等任务。例如,在 Java 的 Stream API
或 Python 的 itertools
、pandas
中,流都能 懒加载数据 并逐步处理,避免一次性加载整个数据集,提高效率。
流的特性:
- 单向传输:数据从 源头 经过 一系列处理,最终形成 结果,不会回溯。
- 懒执行:只有 最终需要数据 时(如
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]
这段代码:
- 创建流 →
names.stream()
- 过滤 → 仅保留以 “A” 开头的元素
- 映射 → 将所有保留的元素转为大写
- 终结操作 →
collect()
触发计算并生成新列表
相比传统 for
循环,流代码更 简洁、声明式、并行友好。
2. 流式编程的优势
流式编程的核心优势主要体现在 简洁性、声明式风格、并行处理 三个方面:
1. 简洁
- 避免显式循环,减少
for
或while
代码,使逻辑更清晰。 - 链式调用,一条语句完成多个数据操作,减少样板代码。
示例(传统循环 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),可以大大提升代码的 可读性 和 执行效率。
结论
- 流(Stream)是一种数据处理方式,不是数据存储容器。
- 流式编程比传统循环更简洁、更声明式、更支持并行处理。
- 流(Stream)一次性迭代,集合(Collection)可重复使用。
- 流式编程适用于数据转换、过滤、并行计算等场景,而 集合适用于数据存储和管理。
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) 中,所有的操作可以分为 两大类:
- 中间操作(Intermediate Operations) → 返回新的流,支持链式调用,懒执行。
- 终端操作(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()
转换流中的每个元素,返回一个新的流。例如:
- 转换类型(
String
→Integer
)。 - 修改数据格式(
"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 终端操作的特点
-
触发流的执行:
- 终端操作是流的“终点”,只有在调用终端操作时,流的中间操作才会真正执行。
- 如果没有终端操作,中间操作不会生效(因为流是惰性求值的)。
-
流关闭:
- 终端操作执行后,流会被关闭,无法再使用。如果尝试复用已关闭的流,会抛出
IllegalStateException
。
- 终端操作执行后,流会被关闭,无法再使用。如果尝试复用已关闭的流,会抛出
-
返回非流类型:
- 终端操作返回的结果通常是非流类型,例如集合、基本类型、
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()
- 功能:将流中的元素收集到一个容器中(如列表、集合、映射等)。
- 返回值:一个容器(如
List
、Set
、Map
等)。 - 示例:
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 终端操作的注意事项
-
流的关闭:
- 终端操作执行后,流会被关闭,无法再使用。如果需要重新处理数据,必须重新创建流。
-
短路操作:
- 某些终端操作(如
findFirst()
、anyMatch()
)是短路操作,一旦找到结果就会立即停止流的处理。
- 某些终端操作(如
-
并行流的线程安全:
- 在并行流中使用终端操作时,确保操作是线程安全的,避免副作用。
2.2.4 小结
终端操作 | 功能 | 返回值 | 示例 |
---|---|---|---|
forEach() | 对每个元素执行操作 | void | stream.forEach(System.out::println) |
collect() | 收集元素到容器 | 容器(如 List 、Set ) | stream.collect(Collectors.toList()) |
reduce() | 组合元素 | Optional<T> 或具体值 | stream.reduce(0, (a, b) -> a + b) |
count() | 统计元素数量 | long | stream.filter(...).count() |
min() / max() | 查找最小/最大值 | Optional<T> | stream.min(Integer::compareTo) |
anyMatch() | 检查是否有元素满足条件 | boolean | stream.anyMatch(num -> num % 2 == 0) |
findFirst() | 查找第一个元素 | Optional<T> | stream.findFirst() |
toArray() | 转换为数组 | Object[] 或指定类型数组 | stream.toArray(Integer[]::new) |
forEachOrdered() | 保证顺序执行操作 | void | stream.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()
- 直接创建并行流
适用于 集合类型(List
、Set
、Map
)。
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()
那么简单。为了从并行流中获得最大的性能提升,下面是一些优化技巧和建议:
-
适用于大数据集:
-
并行流的性能提升在数据量较大的情况下尤为明显。通常建议使用并行流处理 10,000+ 条数据,当数据量较小时,线程的创建与管理开销可能超过并行处理的好处。
-
对于小数据集,顺序流(
stream()
)反而可能表现更好,因为创建线程的开销较大,而且线程之间的上下文切换也会消耗时间。
-
-
避免修改共享变量:
-
并行流的一个重要规则是避免在多个线程之间共享可变状态,尤其是在操作中进行写操作时。并行流中的
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)); // 安全的并行操作
-
-
优先使用
collect()
而非forEach()
:-
在处理并行流时,尽量避免使用
forEach()
操作,尤其是在需要对结果进行收集时。forEach()
并没有顺序收集的保证,这可能会引发数据丢失或顺序不一致的问题。 -
collect()
是一个很好的替代,能够保证顺序收集并且避免并发问题。collect()
操作通常会使用一个线程安全的集合(如ConcurrentLinkedQueue
或Collectors.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)); // 可能引发并发问题
-
-
确保数据的“分割”是均匀的:
-
并行流通过
ForkJoinPool
来自动分割数据。如果数据拆分不均匀,可能导致部分线程的任务量较少,而其他线程任务量过大,从而引发性能瓶颈。因此,确保数据的分割均匀至关重要。 -
比如,对于一个 排序操作,需要确保输入数据已经是可以被合理分割的,否则会浪费线程的计算能力。对于排序操作,Java 内部实现了优化策略,能够确保足够均匀的分割。
-
-
避免不必要的操作:
-
在使用并行流时,某些操作可能导致 线程间竞争 或者 额外的同步开销,这些开销会抵消并行化带来的性能提升。特别是一些 非线程安全 的操作,比如操作共享状态、集合的排序等,尽量避免。
-
如果并行流的任务本身就是 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 小结
- 并行流可以自动利用多核 CPU 提高性能,但不是万能的,适用于 大数据量 + 计算密集型任务。
- 创建方式:
parallelStream()
直接创建并行流。stream().parallel()
转换顺序流为并行流。
- 优化建议:
- 避免修改共享变量(使用
collect()
或reduce()
)。 - 数据量小 不建议使用(线程切换成本高)。
- 有序操作 需使用
forEachOrdered()
保证顺序。
- 避免修改共享变量(使用
- 适用于计算密集型任务,避免用于 IO 操作。