并行流不保证线程安全,问题根源在于共享可变变量的并发读写;应优先使用原子类、线程安全收集器、map-reduce模式或小数据量改用串行流。

并行流本身不制造线程安全,它只是把任务拆开交给多个线程跑;真正出问题的,是你在流里怎么读写变量——尤其是共享可变变量。同步块不是“万能补丁”,它会强行串行化某段逻辑,可能破坏并行初衷,还容易锁错对象、范围过大或遗漏关键点。
先盯住三个高危操作模式
这些写法在线程间共享数据时极易翻车:
-
直接修改外部普通变量:比如用
int sum = 0;,再在forEach里做sum += n——++不是原子操作,两线程可能同时读旧值、各自加1、写回,结果只+1 -
往非线程安全集合写数据:像
ArrayList.add()或HashMap.put()被多个线程并发调用,轻则丢数据,重则抛ConcurrentModificationException或内部结构损坏 -
用 forEach + 共享容器收集结果:例如
list.parallelStream().forEach(item -> result.add(item)),result是普通ArrayList,就等于主动邀请竞争
同步块的实际影响远不止“加个锁”那么简单
它会强制让一段代码变成单线程执行路径,代价和风险都很实在:
-
性能打折明显:如果在
forEach内部每个元素都synchronized(list) { list.remove(...) },等于把并行流退化成“排队干活”,还多出锁开销 -
锁对象选错等于白加:用
this锁实例方法,但多个线程操作的是不同对象实例,锁根本不起作用;用类锁(MyClass.class)又可能过度串行化无关操作 - 范围过大掩盖真实瓶颈:把整个循环体包进同步块,连索引递增、条件判断都被锁住,本可并发的部分也被拖慢
-
无法解决可见性问题:即使加了锁,若没用
volatile或锁保护读写全过程,其他线程仍可能看到过期值
比同步块更推荐的四条出路
多数场景下,换思路比硬加锁更高效、更可靠:
-
用原子类替代基础变量:如
AtomicInteger、AtomicLong,它们靠 CAS 实现无锁原子更新,适合计数、累加等简单聚合 -
用线程安全收集器替代手动写:优先选
Collectors.toConcurrentMap()、toSet()、toList()(返回不可变容器),避免自己碰共享集合 -
把共享写转为线程本地计算:用
map+reduce替代forEach,让每个线程先算局部结果,最后合并,天然规避竞争 - 小数据量直接用串行流:几千条以内字符串处理、数值转换等轻量操作,串行流更稳更快,省去并发调试成本
真要用同步块?必须核对这三点
不是不能用,而是得用对地方、用得克制:
-
只锁真正共享且可变的那部分:例如只锁
map.put(key, value)这一行,而不是整个处理逻辑 - 锁对象必须是所有线程共用的同一实例:静态锁对象、集合自身、或明确声明的 final 对象,避免锁错实例
-
确认操作无状态、数据源支持分片:不能依赖前一次结果,不能用
LinkedList做源(分片慢),终止操作得是collect或reduce这类聚合型操作

















