
java.util.Spliterator 是什么:不是接口,而是并行流的“分片调度员”
它不是为手动创建而生的抽象工具,而是 Spliterator 接口的具体实现载体——JDK 内部用它把数据源(如 ArrayList、HashMap、Stream.iterate())按需切块,再分发给 ForkJoinPool 的线程去处理。你几乎不会直接 new 它,但它的行为会显著影响 parallelStream() 的效率和正确性。
常见错误现象:parallelStream().forEach(...) 结果乱序、性能比串行还差、甚至抛 ConcurrentModificationException —— 很可能就是底层 Spliterator 没法安全拆分,或拆得过碎/过粗。
- 拆分逻辑由
trySplit()控制:返回 null 表示不可再分;否则返回一个新Spliterator,原对象负责前半段,新对象负责后半段 - 是否支持并发访问,取决于
characteristics()返回值:含Spliterator.CONCURRENT才能放心在多线程里遍历同一数据源 -
ArrayList的Spliterator有ORDERED | SIZED | SUBSIZED,所以forEachOrdered()能保序;而HashSet的没有ORDERED,并行遍历时顺序无保证
什么时候要关心 Spliterator:自定义集合 or 自定义 Stream 源
如果你写了继承 AbstractCollection 的类,或用 StreamSupport.stream(Spliterator, boolean) 构造流,就必须提供靠谱的 Spliterator 实现。否则并行流要么卡死,要么跳过元素。
使用场景:ByteBuffer 流式解析、日志文件按块读取、自定义环形缓冲区转流。
立即学习“Java免费学习笔记(深入)”;
- 必须重写
trySplit():不能总是返回 null(那就退化成串行),也不能无脑二分(比如链表二分成本 O(n)) - 必须正确设置
characteristics():比如底层是线程安全队列,就该返回CONCURRENT | NONNULL;若数据源本身无序,别硬加ORDERED - 注意
estimateSize():太小会导致过度拆分(大量小任务开销),太大则并行度不足;对动态数据源,返回Long.MAX_VALUE是常见妥协
parallelStream() 拆分效果差?先看 Spliterator 的 characteristics
并行流不是“开了就快”,它依赖 Spliterator 是否提供足够信息来高效调度。很多慢,并不是 CPU 不够,而是任务分发失衡。
性能影响点:Spliterator.SIZED 缺失 → 无法预估总大小 → ForkJoinPool 用试探性拆分策略,容易生成不均等子任务;Spliterator.SUBSIZED 缺失 → 每次 trySplit() 后都得重新估算子段大小,开销陡增。
- 调试方法:用
stream.spliterator().characteristics()打印值,对照Spliterator常量位掩码(如 64 是SIZED,128 是SUBSIZED) -
Arrays.asList(...).parallelStream()有全部关键特性(ORDERED | SIZED | SUBSIZED | IMMUTABLE),所以表现好;Stream.generate(() -> ...).parallelStream()的Spliterator只有IMMUTABLE | NONNULL,没法预估大小,实际是单线程 fallback - 不要强行给不可分的数据源加并行:比如单个
String调用chars().parallelStream(),底层IntStream.Spliterator虽支持拆分,但每个字符处理太轻量,线程调度成本远超收益
容易踩的坑:Spliterator 不是线程安全的“万能分片器”
它只保证“自己被多个线程分别持有时安全”,不保证“多个线程同时调用同一个实例的方法安全”。这点极易误解。
错误现象:ConcurrentModificationException 在 tryAdvance() 中抛出,或部分元素被跳过。
- 典型误用:把同一个
Spliterator实例传给多个线程,各自调用tryAdvance()—— 这是未定义行为,tryAdvance()通常会修改内部游标,无锁保护 - 正确姿势:每次只由一个线程调用
tryAdvance();拆分靠trySplit()生成新实例,每个实例只被一个线程使用 - 如果数据源本身可变(比如正在被另一个线程写入的
CopyOnWriteArrayList),即使Spliterator有CONCURRENT特性,也只能保证遍历过程不抛 CME,不保证看到最新写入的数据 —— 这是内存可见性问题,得靠volatile或同步机制
真正难的不是写个 Spliterator,而是判断你的数据结构是否值得、能否被安全地并行遍历。很多情况下,老老实实串行 + CompletableFuture 分任务,反而更可控。











