并行流不自动加锁,但显式使用同步机制或共享可变变量会引发锁竞争,导致性能下降甚至比串行更慢;识别信号包括高cpu低吞吐、线程blocked/waiting、临界区过大。

并行流本身不自动加锁,但当你在 lambda 中显式使用 synchronized、ReentrantLock 或共享可变变量(如普通 List、int 计数器)时,实际就引入了锁竞争——这会直接拖慢整体执行,甚至比串行还慢。
识别锁竞争是否正在发生
关键看三个信号:
- CPU 利用率高但吞吐量低:任务耗时明显增长,top 或 VisualVM 显示线程大量处于 BLOCKED 或 WAITING 状态
-
临界区过大:比如在
forEach的 lambda 里对整个ArrayList.add()加锁,每次添加都排队;或反复调用list.size()并据此做分支判断(如示例中if (list.size() ),这种“有状态”访问会让并行失去意义 -
结果不可预测或丢失数据:比如最终收集的元素数量远少于输入,或出现
ConcurrentModificationException,说明多个线程在争抢同一把锁或绕过锁直接操作
量化锁带来的延迟开销
不要只测总耗时,要拆解:
- 用
System.nanoTime()分别记录:锁获取前、进入临界区后、离开临界区前、操作完成后的时刻,计算平均持锁时间 - 统计锁等待次数:
ReentrantLock.getQueueLength()可返回当前等待队列长度;或用 JFR(Java Flight Recorder)抓取jdk.ThreadPark和jdk.JavaMonitorEnter事件 - 对比基准:跑一次纯串行版本(
stream.forEach),再跑并行版,若并行版耗时 ≥ 1.2× 串行版,基本可判定锁已成瓶颈
避免锁竞争的实用替代方案
目标是消除或隔离共享写入:
-
用无状态 + 归约代替共享写:把
list.add(e)改为map(e).filter(...).collect(Collectors.toList()),让并行流内部管理中间容器 -
改用线程安全且无锁的数据结构:例如
ConcurrentLinkedQueue替代加锁的ArrayList;LongAdder替代synchronized counter++ -
分片聚合(reduce/fold):先让每个线程处理子集并生成局部结果(如
Map<k integer></k>),最后用Collectors.toConcurrentMap或手动合并,避免全程争一把锁 -
启用并行流的本地累加器:对计数类场景,用
parallelStream().mapToInt(...).sum(),底层自动使用ForkJoinTask的工作窃取与局部计数,无需显式锁
特别注意隐式锁陷阱
有些操作看似没写锁,实则暗含同步:
-
SimpleDateFormat是非线程安全的,多个线程共用一个实例 = 隐式锁竞争(内部用Calendar缓存) -
StringBuffer方法全加了synchronized,在并行流里频繁调用它拼接字符串,等同于主动排队 - 日志框架如 Log4j 1.x 的
FileAppender默认同步写文件,多线程打日志会严重阻塞










