1.
引言

1.1
多核时代的编程挑战
随着硬件技术的发展,CPU
核心数不断增加,从双核、四核到如今的几十甚至上百核心,单核频率的提升却逐渐逼近物理极限。
如何充分利用多核处理器的计算能力,成为现代编程语言和框架必须面对的课题。
传统的多线程编程模型(如java.lang.Thread、java.util.concurrent包中的ExecutorService)虽然提供了强大的并发能力,但编写正确、高效且易于维护的多线程代码并不容易——开发者需要手动管理线程、处理同步、避免死锁和竞态条件,这些复杂性往往成为程序正确性和性能的瓶颈。
1.2Java
流(Stream)的诞生
Java
Stream
允许开发者以声明式的方式表达对数据集合的复杂操作(如过滤、映射、规约),而无需关心底层的迭代和状态管理。
更重要的是,Stream
API
内置了对并行处理的支持:只需调用.parallel()方法或将集合转换为parallelStream(),即可将一个流操作自动并行化执行。
java
//串行流
.sum();
这种简单性使得并行编程不再是专家的专利,普通开发者也能轻松利用多核性能。
然而,并行流并非银弹,它的背后隐藏着复杂的实现机制和性能陷阱。
如果不理解其工作原理,可能会写出比串行更慢甚至错误的代码。
1.3Java
并行流,从基础用法到内部原理,从性能调优到最佳实践,帮助读者真正“拥抱”并行流,让代码执行速度飞起。
全文约
万字,涵盖以下内容:
并行流的基本概念与创建方式
底层
Fork/Join
框架与工作窃取算法
并行流操作的特性和限制
性能影响因素与基准测试方法
常见陷阱与避坑指南
最佳实践与设计模式
高级主题:自定义
Spliterator、与
结合等
实战案例:处理百万级日志、并行图像滤波等
总结与未来展望
2.并行流基础
2.1
中,流(Stream)代表一个支持顺序和并行聚合操作的数据元素序列。
流本身不存储数据,而是从数据源(如集合、数组、I/O
资源)中获取,并通过流水线式的操作链进行处理。
串行流:所有操作在单线程中顺序执行,元素按遇到顺序(encounter
order)依次处理。
并行流:将数据分割成多个片段,由多个线程并发处理每个片段,最后将各个片段的处理结果合并起来。
2.2
创建并行流
有两种常见的方式获得并行流:
从集合创建并行流:
Collection接口提供了parallelStream()方法,直接返回一个可能的并行流。java
List<String>
list
list.parallelStream();
将串行流转换为并行流:通过调用已存在的流的
parallel()方法。java
Stream<String>
stream
stream.parallel();
parallel()方法返回一个等效的并行流(如果流已经是并行的,则返回自身)。同样,
sequential()方法可将并行流转为串行。流可以在这两种模式之间切换多次,但最后一次调用决定了最终执行模式。
java
stream.parallel()
.filter(...)
是并行执行
注意:流的整个流水线(pipeline)以最后一个模式设置(
parallel()或sequential())为准,且该设置影响整个流水线,而不是单个操作。
2.3
第一个并行流示例
考虑一个简单的任务:计算
10,000,000
的所有整数的和。
使用串行流和并行流分别实现并比较时间。
java
importpublic
核)上输出:
text
串行和:50000005000000,
倍以上,且代码几乎无改动。
这展示了并行流在计算密集型任务上的巨大潜力。
3.
并行流的内部机制
并行流是如何实现自动并行化的?其核心是
Java
框架,它是对传统线程池的增强,特别适合分治(divide-and-conquer)风格的并行任务。
3.1
Fork/Join
框架的核心思想是将一个大任务(task)拆分成多个小任务(fork),让多个线程并发执行这些小任务,然后将所有小任务的结果合并(join)起来得到最终结果。
这个过程递归进行,直到任务小到可以直接顺序执行。
框架由两个核心类组成:
ForkJoinPool:一个特殊的ExecutorService,用于管理ForkJoinTask的执行。ForkJoinTask:代表一个可拆分任务的抽象类,常用子类为RecursiveTask<V>(有返回结果)和RecursiveAction(无返回结果)。
3.2
工作窃取(Work-Stealing)算法
Fork/Join
框架最精妙的设计是工作窃取算法。
每个工作线程维护一个双端队列(deque),用于存放分配给自己的任务。
当线程完成自己的任务后,会尝试从其他线程的队列尾部“窃取”一个任务来执行,从而保持所有线程的负载均衡。
这种机制减少了线程因空闲而浪费的资源,提高了
CPU
利用率。
在并行流的实现中,数据源被分割成多个片段,每个片段作为一个子任务提交给公共的ForkJoinPool。
线程处理自己的片段,并可能窃取其他片段的剩余部分。
3.3
ForkJoinPool
默认情况下,所有并行流共享同一个ForkJoinPool实例,称为公共池(common
pool)。
可以通过ForkJoinPool.commonPool()获取该实例。
公共池的大小默认为
CPU
1(Runtime.getRuntime().availableProcessors()
核机器上,公共池大小为
7。
主线程也会参与执行,因此实际并发线程数为池大小
+
1。
这种共享设计简化了使用,但也带来一些问题:
如果多个并行流同时执行,它们会竞争公共池中的线程,可能导致性能下降。
如果某个并行流执行了阻塞操作(如
I/O),会占用池中线程,影响其他并行流的执行。
可以通过
JVM
参数调整公共池大小:
text
-Djava.util.concurrent.ForkJoinPool.common.parallelism=8
3.4自定义
ForkJoinPool
在某些场景下,需要隔离并行流,避免相互干扰。
可以创建自定义的ForkJoinPool并提交任务:
java
ForkJoinPoolcustomPool
}
注意:这种写法要求提交的任务是Callable或Runnable,且流操作必须完全在任务内部完成。
parallel()流仍然会使用当前线程的ForkJoinPool?实际上,并行流的执行线程由调用它的线程的ForkJoinPool决定:如果在自定义池中提交任务,且在该任务中创建并行流,则并行流会使用该自定义池。
这是因为ForkJoinTask的fork()方法会将任务提交给当前线程所在的ForkJoinPool。
3.5
流的拆分:Spliterator
并行流如何将数据源拆分成多个片段?这依赖于Spliterator(可拆分迭代器)接口。
Spliterator是
Java
引入的,用于遍历和分割数据源。
它有两个关键方法:
tryAdvance(Consumer<?super
action):尝试处理下一个元素,如果存在则执行
action
true。
trySplit():尝试将当前数据源分割成两部分,返回一个新的Spliterator代表第二部分,当前Spliterator则代表第一部分。如果无法分割(如只剩一个元素),返回
null。
并行流的底层会递归调用trySplit(),直到分割出的子任务足够小(通常根据阈值判断),然后每个子任务由不同线程处理。
不同数据源的拆分能力不同,直接影响并行性能。
例如:
ArrayList、数组、IntStream.range:可以完美拆分(基于索引),拆分成本低。LinkedList、Stream.iterate:难以高效拆分(需要遍历),通常导致较差的并行性能。HashSet、TreeSet:拆分成本中等,基于内部结构(如红黑树)可能有一定拆分能力。
4.
有状态与无状态操作
流操作可以分为两大类:
无状态操作:每个元素的处理不依赖于其他元素,如
filter、map、flatMap、forEach、peek等。这些操作在并行流中天然安全,因为每个元素独立。
有状态操作:处理过程需要记录之前处理过的元素信息,如
distinct、sorted、limit、skip等。这些操作在并行流中需要额外的协调和存储,可能成为性能瓶颈或导致结果不确定。
例如,sorted操作在并行流中会将各个子任务排序,然后合并排序结果(类似归并排序)。
这虽然能并行,但合并开销不容忽视。
limit操作在并行流中尤其复杂,因为需要全局地截取前
个元素,但元素在多个线程中分布,实现困难且通常表现不佳。
4.2
中间操作与终端操作
流操作分为中间操作(返回新流)和终端操作(产生结果或副作用)。
并行流的真正执行发生在终端操作被调用时,此时整个流水线会并行执行。
常见的终端操作:
规约(reduction):
reduce、sum、max、min、count等。这些操作通常能很好地并行化,因为它们通过结合律(associativity)将部分结果合并。
收集(collect):
collect方法将元素累积到可变容器(如List、Set、Map)。如果使用
Collectors提供的收集器,大多数支持并发收集(如toList()不是线程安全的,但toConcurrentMap()是)。迭代(iteration):
forEach、forEachOrdered。forEach不保证遇到顺序,适合并行;forEachOrdered保证顺序,但会牺牲部分并行性。匹配与查找:
anyMatch、allMatch、noneMatch、findFirst、findAny。这些操作在并行流中可能提前终止(短路),实现复杂但通常高效。
4.3
哪些操作适合并行?
并非所有操作都适合并行。
适合并行的操作通常满足:
无状态:如
filter、map。可结合(associative):规约操作(如加法、乘法、最大值、最小值)满足结合律,可以安全并行。
低合并成本:如将多个部分结果合并成最终结果的开销很小(例如数值加法),而合并两个大型
Map可能很昂贵。
不适合并行的操作:
有高度依赖性:如
sorted虽然可以并行,但数据量很大时合并排序开销大;limit很难并行。依赖顺序:
findFirst在并行流中需要额外协调以返回第一个元素,通常比findAny慢。高开销的合并:如
collect到非并发容器(如ArrayList)时,每个线程创建独立容器,最后合并,合并ArrayList需要复制元素,成本高。
4.4
避免干扰和保持无状态
并行流要求操作的行为必须:
不干扰数据源:不要在流操作中修改数据源(除非通过线程安全的并发集合,且理解后果)。
例如,不能在
forEach中向原List添加元素,这会导致不确定行为甚至ConcurrentModificationException。无状态:
lambda表达式不应依赖外部可变状态,也不应修改外部状态(除非是线程安全的)。例如:
java
//
错误示例:修改共享变量
list.parallelStream().forEach(x
->
list.parallelStream().reduce(0,
Integer::sum);
即使是无状态操作,也要注意lambda内部捕获的变量最好是final或
effectively
final,且避免修改共享可变对象。
5.
性能考量
并行流并不是万能的,错误使用可能导致性能比串行更差。
理解影响性能的关键因素,才能有效利用并行流。
5.1
数据量大小
并行化本身有开销:拆分任务、线程调度、合并结果。
如果数据量太小,这些开销可能超过并行计算带来的收益。
通常,数据量越大,并行化的收益越明显。
一个经验法则是:只有当数据量达到几千或几万以上,且每个元素处理有一定计算量时,才考虑并行。
5.2
每个元素处理的计算量
如果每个元素处理的计算量很少(如简单的加法),并行化可能无法弥补线程协调的成本。
反之,如果计算量很大(如复杂的数学计算、数据库查询),并行化能显著提速。
考虑两种场景:
计算密集型:CPU
长时间运算,适合并行。
I/O
CPU,但并行流使用的
ForkJoinPool没有为阻塞任务优化,可能导致线程饥饿。此时应使用自定义线程池(如
Executors.newFixedThreadPool)结合CompletableFuture更合适。
5.3
装箱开销
基本类型流(IntStream、LongStream、DoubleStream)避免了装箱拆箱,性能优于对象流。
在并行流中,这种差异更明显,因为装箱会创建大量临时对象,增加
使用对象流(Stream<Integer>)
Stream.of(1,
3).parallel().map(...)
应优先使用基本类型流处理数值数据。
5.4
数据源的可分解性
数据源能否被高效拆分是并行性能的关键。
Spliterator的拆分能力直接影响负载均衡。
常见数据源的可分解性:
优良:
ArrayList、数组:基于索引,拆分时间复杂度O(1),能均匀拆分。
IntStream.range:同样基于索引,完美拆分。ConcurrentHashMap、ConcurrentLinkedQueue:并发集合通常提供较好的拆分器。
一般:
HashSet、TreeSet:基于哈希表或树,拆分需要遍历部分元素,但可能有一定结构性支持。Stream.iterate:无限流,依赖前一个元素,无法拆分,只能顺序处理。
较差:
LinkedList:每个元素只知道下一个,拆分需要遍历到中间位置,时间复杂度O(n),基本无法有效并行。
Stream.generate:无限流,无序,但本身不支持拆分(除非自定义)。
因此,在并行流中,优先选择ArrayList或数组作为数据源。
避免使用LinkedList。
5.5
合并成本
规约操作的合并成本取决于结果类型。
例如:
数值求和:合并两个部分和只是简单加法,成本极低。
收集到
List:如果使用Collectors.toList(),每个线程创建独立ArrayList,最后合并时需要复制所有元素,成本为O(n),这可能抵消并行收益。
收集到
Set:合并HashSet需要插入所有元素,同样提供了并发收集器以减少合并开销,如:
Collectors.toConcurrentMap():使用ConcurrentHashMap,允许多线程同时插入,无需最终合并。Collectors.groupingByConcurrent():并发分组。
对于规约,可以使用
collect的自定义版本,提供并发容器(如ConcurrentSkipListSet)和合并函数。5.6
内存局部性
现代
依赖缓存提高性能。
顺序访问内存(如遍历数组)具有良好的空间局部性,能高效利用缓存。
并行流将数据分散到多个线程,可能导致不同线程访问不同内存区域,但整体仍然是顺序访问各自片段,所以缓存友好性通常不错。
但如果数据源是链表,每个元素随机分布在内存中,缓存命中率低,串行性能已不佳,并行也不会改善。
5.7
基准测试示例
我们来设计一个简单的基准测试,比较不同数据源、不同操作下串行与并行的性能。
使用
JMH(Java
Harness)是更科学的方式,但为了演示,这里用简单的计时。
场景
1:对
中的数字求和
java
List<Integer>
list
list.stream().mapToInt(Integer::intValue).sum();
long
list.parallelStream().mapToInt(Integer::intValue).sum();
end
求和
将数据源换为
LinkedList,同样数据量。场景
3:包含复杂计算
对每个元素执行一些计算(如
Math.sin、Math.cos),增加
CPU
且元素计算简单,并行加速比通常在核心数附近;对于
LinkedList,串行可能比并行更快,因为拆分成本太高。
6.
线程安全问题
并行流的多线程环境要求操作是线程安全的。
常见的错误是在
forEach或peek中修改共享可变状态。java
//
错误:共享
Collections.synchronizedList(new
或使用
ConcurrentLinkedQueue<>();
collect
内部使用线程局部容器,最后合并,安全且高效
6.2
顺序的不确定性
并行流的非顺序操作(如
forEach)不保证遇到顺序(encounterorder)。
如果业务依赖顺序,应使用
forEachOrdered,但这会强制部分同步,降低并行性。java
IntStream.range(0,
10)
.forEachOrdered(System.out::print);
保持顺序输出
即使使用
forEachOrdered,也不能保证像串行流那样高效,因为必须维护顺序。6.3
并发修改异常
在并行流中修改数据源是危险的。
即使单线程流,也不允许在迭代过程中修改非并发集合。
并行流更是如此,因为多个线程可能同时修改。
java
List<String>
list
ArrayList<>(Arrays.asList("a",
"b",
list.parallelStream().forEach(s
->
ConcurrentModificationException
});
如果确实需要修改,可以使用并发集合(如
CopyOnWriteArrayList、ConcurrentHashMap),但要理解其行为(如CopyOnWrite
的每次修改复制数组,性能差)。
6.4
限制并行度
默认并行度基于
核心数,但有时需要手动调整。
通过系统属性
java.util.concurrent.ForkJoinPool.common.parallelism可以全局调整,但可能影响其他并行流。更好的方法是使用自定义
ForkJoinPool(见3.4
节)。
另一个技巧是使用
-Djava.util.concurrent.ForkJoinPool.common.parallelism=1来关闭所有并行流(用于调试或低资源环境)。6.5
I/O
操作(如文件读写、网络请求),问题就复杂了。
I/O
操作会使线程阻塞,而
设计用于计算密集型,阻塞会导致线程被占用,无法执行其他任务,甚至可能造成饥饿(因为池大小固定)。
此时,使用并行流通常不是最佳选择。
替代方案:
使用
CompletableFuture结合自定义线程池(如Executors.newCachedThreadPool()),配合异步I/O
操作移到流外部,或者分批处理。
6.6
调试复杂性
并行流的调试比串行困难,因为执行线程不确定,堆栈跟踪混乱。
可以使用
forEach打印当前线程名来观察:java
list.parallelStream().forEach(x
->
System.out.println(Thread.currentThread().getName()
+
x));
输出显示元素被多个线程(如
ForkJoinPool.commonPool-worker-1)处理。
如果需要调试特定元素,可以借助
peek,但要注意peek是中间操作,只有终端操作触发时才会执行。6.7
资源耗尽
并行流默认使用公共池,如果同时运行多个并行流,可能耗尽池中线程,导致所有流变慢。
例如,在
Web
应用中,每个请求都使用并行流处理数据,公共池可能被大量请求阻塞。
解决方案:
为不同任务分配不同的自定义
ForkJoinPool。或者改用传统线程池
+
何时使用并行流
根据前面的讨论,可以总结出适合并行流的场景:
数据量大(至少几千个元素)。
每个元素处理的计算量较大(非
trivial)。
数据源易于拆分(如
ArrayList、数组、IntStream.range)。
操作是无状态的,或规约操作满足结合律。
合并成本低(数值加法、收集到并发容器)。
不涉及
I/O
何时避免使用并行流
数据量小。
数据源拆分成本高(如
LinkedList、Stream.iterate)。
操作有强顺序要求(如
limit、findFirst)。
需要频繁合并且合并成本高(如收集到
ArrayList)。
包含阻塞操作(I/O、锁等待)。
在共享公共池的环境中运行多个并行流(可能相互干扰)。
7.3
使用并行流进行规约和收集
规约
使用
reduce或基本类型流的sum、max等。确保累加器函数是关联的(associative)。
java
int
sum
numbers.parallelStream().reduce(0,
Integer::sum);
注意:
reduce的第一个参数是恒等值(identity),对于加法是1。
对于非交换但结合的操作(如字符串连接),也要小心顺序。
收集
Collectors提供了许多收集器。对于并行流,应优先使用支持并发收集的收集器:
toList():非并发,内部使用ArrayList,但通过多个线程的局部列表最后合并,安全但合并成本高。toSet():类似,合并成本高。toConcurrentMap():并发,使用ConcurrentHashMap。groupingByConcurrent():并发分组。
java
Map<Integer,
map
.collect(Collectors.groupingByConcurrent(String::length));
如果必须使用
toList(),且数据量很大,可以考虑使用collect的三参数版本,提供并发容器(如ConcurrentLinkedQueue),但注意最终结果类型可能不是List。
java
ConcurrentLinkedQueue<Integer>
queue
.collect(ConcurrentLinkedQueue::new,
Queue::add,
实现高效并行收集
有时标准收集器不满足需求,可以自定义
Collector。实现时需要注意:
supplier():提供可变结果容器,对并行流来说,每个线程会调用supplier
获取自己的容器。
accumulator():将元素添加到容器。combiner():合并两个容器的内容,用于最终合并。finisher():将中间容器转换为最终结果(可选)。characteristics():定义收集器的特性,如IDENTITY_FINISH、CONCURRENT、UNORDERED。如果标记
CONCURRENT,则表示容器本身支持并发添加(如ConcurrentHashMap),此时accumulator
步骤。
标记
UNORDERED表示收集不关心顺序,可能提高并行效率。
7.5
使用并行流与并发集合
当需要将结果直接存入共享集合时,可以使用并发集合,但要注意并发集合的迭代器是弱一致的,可能不反映最新修改。
在并行流中,通常不推荐在
forEach中更新外部集合,而是用collect。8.
高级主题
8.1
的任务,可以考虑组合使用并行流和
CompletableFuture。例如,先使用并行流处理
CPU
部分提交给自定义线程池异步执行。
java
List<Integer>
ids
List<CompletableFuture<String>>
futures
CompletableFuture.supplyAsync(()
->
.collect(Collectors.toList());
这里,
parallelStream用于并行创建多个CompletableFuture(创建过程轻量),而实际I/O
在另一个线程池中执行,避免阻塞公共池。
8.2
自定义
Spliterator
如果要处理的数据源不是标准集合,可以自定义
Spliterator来支持高效的并行拆分。例如,处理一个大文件,可以自定义
Spliterator按行拆分,每个子任务读取文件的一部分。实现
Spliterator需要实现四个方法:tryAdvance:消费一个元素。trySplit:分割当前部分,返回新的Spliterator。
estimateSize:估计剩余元素数量(用于负载均衡)。characteristics:返回特征值,如SIZED、SUBSIZED、ORDERED、DISTINCT、IMMUTABLE、CONCURRENT等。
示例:一个简单的数组拆分器
java
class
implements
action.accept(array[start++]);
return
ArraySpliterator<>(array,
start,
}
然后可以通过
StreamSupport.stream(spliterator,并行流的底层实现解析
深入
ReferencePipeline类的源码,可以看到并行流的执行流程大致如下:终端操作调用
evaluate方法,传入ParallelOp或TerminalOp。构建
Task(如ReduceTask、ForEachTask),继承自CountedCompleter。任务提交到当前
ForkJoinPool执行。compute方法中,如果当前任务足够小,则顺序执行;否则调用trySplit分割,创建子任务并fork,然后等待所有子任务完成,最后合并结果。
使用工作窃取算法动态平衡负载。
8.4
Java
8,但后续版本也带来了一些增强:
Java
9:增加了
takeWhile、dropWhile、ofNullable等操作。这些操作在并行流中的行为需要理解:
takeWhile在并行流中可能不是短路所有线程,而是每个线程独立截取,最终合并时可能包含不符合条件的元素,因此实际使用中应谨慎。Java
10:收集器新增
toUnmodifiableList等。Java
11:无重大变化。
Java
12:
Collectors.teeing等。Java
16:
Stream.toList()作为终端操作,与collect(Collectors.toList())类似,但更简洁。对于并行流,
toList()同样通过合并多个ArrayList实现,可能不如collect(toConcurrentList())(如果有的话)高效。
9.
实战案例
9.1
处理百万级日志文件
假设有一个大型日志文件,每行包含时间戳、日志级别、消息等。
需要统计
ERROR
万行。
串行方式:逐行读取,正则匹配,统计。
可能耗时数分钟。
并行流方式:可以利用
Files.lines()获得行的流,然后并行处理。java
import
java.nio.file.Files;
java.time.format.DateTimeFormatter;
import
DateTimeFormatter.ofPattern("yyyy-MM-dd
HH:mm:ss");
Files.lines(Paths.get(logFile)))
errorCountByHour
line.contains("ERROR"))
.map(line
LocalDateTime.parse(timestampStr,
formatter);
dt.withMinute(0).withSecond(0).withNano(0);
按小时整点
.collect(Collectors.groupingByConcurrent(
));
System.out.println(errorCountByHour);
}
注意:
使用
Files.lines()返回的流需要及时关闭(try-with-resources),它内部持有文件句柄。并行流会利用公共池,如果
CPU
核心多,速度很快。
正则匹配
contains比较轻量,但如果需要更复杂的解析,可以考虑更高效的解析器。
9.2
并行图像滤波
图像处理是典型的计算密集型任务。
假设有一批图片(每个
1920x1080),需要对每个像素应用高斯模糊滤镜。
可以并行处理每张图片,甚至并行处理一张图片内的像素块。
使用并行流处理图片列表:
java
List<Path>
imagePaths
.collect(Collectors.toList());
如果要并行处理一张图片的像素,可以创建一个包含所有像素坐标的流,并行计算。
但要注意像素数可能很大(如
200
万),且每个像素计算独立,适合并行。
java
BufferedImage
image
});
上面的代码存在线程安全问题,因为多个线程同时修改
image对象。解决方案:
使用并发数据结构,但
BufferedImage不支持。创建多个子图像,分别处理,最后合并(复杂)。
使用
Arrays.parallelSetAll来处理像素数组。
通常,并行处理图像更适合将图像拆分成多个区域,每个区域分配给一个线程,区域间不重叠,避免竞争。
9.3
并行排序
Java
的
Arrays.parallelSort()使用Fork/Join
中的
sorted()在并行流中也能利用并行排序,但限于流操作。例如,对一个大数组排序:
java
int[]
array
IntStream.of(array).parallel().sorted().toArray();
这等价于
Arrays.parallelSort(array),但多了一次数组复制(因为流输出到新数组)。如果可以直接原地排序,使用
Arrays.parallelSort更高效。对于对象流,
sorted()内部使用了Arrays.sort的并行版本?实际上,流中的sorted在并行时会收集到数组,然后调用Arrays.parallelSort,最后生成新流,所以效率也不错。10.
总结与展望
10.1
的并行流为开发者提供了一种简单而强大的并行编程模型。
它降低了多线程编程的门槛,使得只需少量代码就能利用多核处理器。
在合适的场景下,并行流可以显著提升程序性能,同时保持代码的可读性和可维护性。
然而,并行流并非万能。
它需要开发者理解数据源、操作特性、性能影响因素,并遵循最佳实践。
盲目使用并行流可能导致性能下降、资源耗尽甚至数据错误。
10.2
Java
平台一直在演进,未来可能会带来更先进的并行编程模型。
例如:
Project
Java
中引入)旨在提供轻量级虚拟线程(virtual
threads),简化并发编程。
虚拟线程可以大量创建,阻塞成本极低,有望改变
I/O
密集型任务的编程模式。
届时,并行流可能会与虚拟线程结合,更高效地处理阻塞操作。
Vector
API(孵化中)允许利用
CPU
指令进行数据并行计算,进一步提升数值计算性能。
结构化并发(Structured
Concurrency)提供更好的任务管理和错误处理。
尽管新特性不断涌现,并行流作为
Java
标准库的一部分,仍将在可预见的未来发挥重要作用。
理解其原理和适用场景,是每个
Java
参考文献与进一步阅读
Oracle
Java
Parallelism
Brian
Goetz
2006.
Raoul-Gabriel
Urma
源码:
java.util.stream包,java.util.concurrent.ForkJoinPool。


