collect 动作天然遵循“先分段本地收集、再合并结果”的分治三步:拆(Fork)、收(Local collect)、合(Join);它通过 Supplier→BiConsumer→BinaryOperator 三元组实现无锁并发,要求操作可交换、可结合且数据源支持高效分割。

并行流中的 collect 动作,是理解“分治原理”最直观的入口——它不是简单地把顺序操作搬到多线程里,而是天然遵循“先分段本地收集、再合并结果”的两阶段逻辑。
collect 的分治三步:拆、收、合
当你调用 list.parallelStream().collect(Collectors.toList()) 时,JVM 并不会让所有线程往同一个 ArrayList 里加元素(那会引发严重竞争)。实际过程是:
- 拆(Fork):数据源被 ForkJoinPool 按 spliterator 切成若干子段,每个线程拿到一段独立数据
-
收(Local collect):每个线程在自己线程内,用一个本地
ArrayList(或HashMap)完成收集,完全无锁、无同步 -
合(Join):各线程的本地容器通过
putAll、addAll或merge等方式合并成最终结果
变量处理为什么必须“本地化”
你不能在 parallelStream().forEach(...) 中直接修改外部变量(如 int sum = 0; ... sum += x),因为这会引发竞态。而 collect 的设计绕开了这个问题:
- 它不依赖共享变量,而是靠“供应商(Supplier)→ 累加器(BiConsumer)→ 合并器(BinaryOperator)”三元组定义行为
- 例如
Collectors.summingInt(Integer::intValue),内部为每个线程提供独立的long累加器,最后才相加 - 你自己写自定义 Collector 时,
supplier()返回的是新容器,combiner()负责安全合并——这就是分治对变量生命周期的约束
分组场景下的分治体现最明显
比如按部门分组:.collect(Collectors.groupingBy(Employee::getDept))
- 线程 A 处理 [张三, 李四] → 得到
{"研发": [张三], "测试": [李四]} - 线程 B 处理 [王五, 赵六] → 得到
{"研发": [王五, 赵六]} - 合并时不是“逐个 put”,而是对每个 key 执行
map1.get(k).addAll(map2.get(k)),键冲突由 merge 逻辑保障 - 注意:
groupingByConcurrent()强制全程用ConcurrentHashMap,反而放弃分治优势,导致每个线程都背锁开销
关键提醒:分治有效,前提是可合并
不是所有操作都适合 collect 分治。以下情况会破坏分治逻辑:
- 累加器函数(accumulator)修改了外部状态(如 static 变量、IO 写入)
- 合并器(combiner)不可交换或不可结合(如用
LinkedList.addFirst()就会错乱顺序) - 数据源本身不支持高效分割(如
LinkedList的 spliterator 效率极低,切分成本远超收益)

















