并行流中执行有副作用的Consumer必然导致变量状态不可预测,因其修改外部可变变量时多线程非原子操作引发竞态;应改用collect、原子类型或线程安全容器等无副作用方式。

并行流中执行有副作用的 Consumer,会导致变量最终状态不可预测——这不是“偶尔出错”,而是由并发本质决定的必然结果。关键不在于“怎么修”,而在于“为什么一定不准”。
副作用 Consumer 在并行流里为何必然失效
当 Consumer 修改外部可变变量(如普通 List、int、HashMap 或自定义对象字段)时,多个线程会同时执行该操作。而这些操作通常不是原子的:
-
list.add(x)包含“检查容量→复制数组→写入元素”多步,中间可能被其他线程打断 -
sum += x等价于sum = sum + x,需读取、计算、写回三步,两线程同时读到旧值,各自加后写回,造成一次丢失 -
map.put(k, v)若未用ConcurrentHashMap,同样存在结构修改冲突和数据覆盖风险
如何观察和验证这种不确定性
不要依赖单次运行结果。可用以下方式暴露问题:
- 用固定小数据集(如
Arrays.asList(1,2,3,4,5))反复执行 100 次并行流 + 副作用 Consumer,统计结果分布——大概率出现多个不同值 - 在 Consumer 内加入
Thread.sleep(1)或日志打印线程名(Thread.currentThread().getName()),能直观看到多线程交叉执行顺序 - 用
jstack抓取运行中的线程堆栈,确认是否发生ConcurrentModificationException或死锁等待
替代方案:安全地达成相同目的
目标不是“禁用 Consumer”,而是用无副作用或线程安全的方式实现逻辑:
- 改用
collect:例如parallelStream().map(...).collect(Collectors.toList()),让框架负责线程安全合并 - 用原子类型:对计数类场景,用
AtomicInteger、AtomicLong替代普通变量 - 用线程安全容器:如
ConcurrentHashMap、CopyOnWriteArrayList(注意后者适合读多写少) - 把副作用移到终端操作:如用
forEachOrdered强制按序执行(牺牲并行性,仅适用于必须顺序且数据量不大时)
一个典型误用与修正对比
错误写法(结果随机):
Listwords.parallelStream().forEach(s -> result.add(s.toUpperCase()));
正确写法(结果确定、保持并行):
List .map(String::toUpperCase) .collect(Collectors.toList());










