并行流使用注意事项
在 Java 中,并行流 是一种利用多核处理器并行处理数据的强大工具。尽管并行流可以提高性能,但使用时需要注意以下几点:
1.1 线程安全
-
无状态操作:确保流中的操作是无状态的。这意味着操作不应依赖于外部可变状态,以避免线程间的竞争条件。使用无状态的 Lambda 表达式或方法引用。
-
使用并发集合:如果操作需要共享数据,使用线程安全的集合(如
ConcurrentHashMap或CopyOnWriteArrayList)来避免数据不一致性。
1.2 数据集大小
-
适合大数据集:并行流在处理小数据集时可能会引入额外的开销(如线程创建和上下文切换),导致性能下降。建议在处理大数据集时使用并行流。
-
使用
parallelism设置:可以通过设置并行度来优化性能(例如,使用ForkJoinPool的setParallelism()方法)。默认情况下,ForkJoinPool 的并行度与可用的处理器核心数相同。
1.3 处理顺序
-
顺序保证:并行流不保证操作的顺序。如果需要保持顺序,使用
.sequential()方法将流转换回顺序流,或使用forEachOrdered()方法进行有序处理。 -
避免短路操作:对于并行流,避免使用短路操作(如
findFirst()或anyMatch()),因为这些操作会导致提前终止并影响性能。
1.4 资源管理
-
避免过度并发:当流操作涉及大量 I/O 操作(如文件读写、数据库访问)时,过多的线程可能会导致资源竞争和性能下降。根据需要调整并行度。
-
异常处理:在并行流中,异常处理需要特别小心。捕获异常时,可能会丢失部分异常信息。考虑使用
try-catch块包裹操作,或使用peek()方法进行调试。
1.5 性能监控
- 性能监控工具:使用性能监控工具(如 VisualVM、Java Flight Recorder 等)监控并行流的执行情况,以识别潜在的性能瓶颈。
示例
以下是一个使用并行流处理集合的示例:
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用并行流计算总和
int sum = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum: " + sum);
}
}
sum这种操作当然数据越大,并行流数量越多效率越高
并行流的底层是fork,join
Fork/Join 是 Java 7 引入的一种用于并行处理任务的框架,属于 Java 并发包(java.util.concurrent)的一部分。它被设计用于利用多核心处理器来提高性能,尤其适合于大规模数据处理和递归算法。以下是 Fork/Join 的底层原理和工作机制的详细介绍。
1. Fork/Join 框架概述
- Fork:将一个大任务分解成多个小任务(子任务)。
- Join:合并子任务的结果,得到最终结果。
Fork/Join 框架的核心类是 ForkJoinPool 和 ForkJoinTask。
2. 核心组件
2.1 ForkJoinPool
- 工作线程:
ForkJoinPool创建了一组工作线程,每个线程都有一个工作队列。工作线程用于执行任务并处理子任务。 - 工作窃取:Fork/Join 框架采用工作窃取算法,允许空闲线程从其他忙碌线程的队列中“窃取”任务。这种机制提高了线程利用率,避免了线程闲置。
- 分治算法:Fork/Join 框架非常适合分治算法,通过递归地将任务分解为更小的子任务来实现并行处理。
2.2 ForkJoinTask
- 抽象类:
ForkJoinTask是一个抽象类,提供了任务的基本功能。 - 子类:有两个常用的子类:
RecursiveAction:用于没有返回值的任务。RecursiveTask<V>:用于有返回值的任务。
3. 工作原理
Fork/Join 框架的工作原理可以分为以下几个步骤:
3.1 任务分解
- 当提交一个
ForkJoinTask任务时,ForkJoinPool会将其放入一个线程的工作队列中。 - 如果任务足够大,它会调用
fork()方法将任务分解为多个子任务。每个子任务会被递归地分解,直到达到可以直接处理的大小(通常是一个阈值)。
@Override
protected V compute() {
if (shouldSplit()) {
// 分解任务
RecursiveTask<V> subtask1 = new SubTask1();
RecursiveTask<V> subtask2 = new SubTask2();
// Fork (分解)
subtask1.fork();
subtask2.fork();
// Join (合并)
V result1 = subtask1.join();
V result2 = subtask2.join();
return combine(result1, result2);
} else {
return baseComputation();
}
}
3.2 任务调度
- 每个工作线程从自己的工作队列中取任务执行。如果工作队列为空,线程会尝试从其他线程的工作队列中“窃取”任务。
- 工作窃取算法通过将多余的任务从一个线程的队列中移到空闲线程的队列来实现负载均衡。
3.3 结果合并
- 一旦子任务完成,线程会调用
join()方法等待子任务完成并获取结果。 - 最终结果是通过合并所有子任务的结果来得到的。
4. 线程管理
- 线程池:
ForkJoinPool使用了固定数量的工作线程,通常是可用处理器的数量。这些线程会被重复利用,而不是每次都创建新的线程,从而减少了线程创建和销毁的开销。 - 工作窃取:通过工作窃取算法提高了线程的利用率,避免了线程闲置的情况。
5. 示例代码
以下是一个简单的例子,演示如何使用 Fork/Join 框架计算数组的总和:
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class ForkJoinSum extends RecursiveTask<Long> {
private final long[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 1000;
public ForkJoinSum(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 直接计算
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// 分解任务
int mid = (start + end) / 2;
ForkJoinSum leftTask = new ForkJoinSum(array, start, mid);
ForkJoinSum rightTask = new ForkJoinSum(array, mid, end);
// Fork
leftTask.fork();
// Join
long rightResult = rightTask.compute();
long leftResult = leftTask.join();
return leftResult + rightResult;
}
}
public static void main(String[] args) {
long[] array = new long[10000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1; // 1到10000
}
ForkJoinPool pool = new ForkJoinPool();
ForkJoinSum task = new ForkJoinSum(array, 0, array.length);
long result = pool.invoke(task);
System.out.println("Total sum: " + result); // 输出:Total sum: 50005000
}
}