340. Java Stream API - 理解并行流的额外开销

340. Java Stream API - 理解并行流的额外开销

------ 并行,不等于免费午餐!


🚀 并行流的潜在收益是有代价的!

使用 parallelStream() 看似能提升性能,但背后要付出不少额外开销

  1. 数据必须被拆分成子任务
  2. 子任务由多个线程并发执行
  3. 所有子任务的结果需要合并

⚠️ 如果你的任务本来就很轻量,或者数据不适合拆分,那么使用并行流反而会更慢!


📦 拆分数据的开销

数据的拆分(Splitting)不是"白送的",它会消耗资源:

  • 易于拆分的源 :比如 ArrayList,因为支持按索引随机访问
  • 难以拆分的源 :比如 LinkedList,只能顺序遍历,拆分非常低效

🔍 举例:

java 复制代码
List<Integer> arrayList = IntStream.range(0, 1_000_000)
                                   .boxed()
                                   .collect(Collectors.toCollection(ArrayList::new));

List<Integer> linkedList = IntStream.range(0, 1_000_000)
                                    .boxed()
                                    .collect(Collectors.toCollection(LinkedList::new));

long t1 = System.nanoTime();
arrayList.parallelStream().map(x -> x * 2).toList();
System.out.println("ArrayList time: " + (System.nanoTime() - t1));

long t2 = System.nanoTime();
linkedList.parallelStream().map(x -> x * 2).toList();
System.out.println("LinkedList time: " + (System.nanoTime() - t2));

📉 你会发现 LinkedList 表现很差,因为拆分难!


⚙️ 合并结果的成本

并行处理完后,所有子线程都要把结果汇总回来,这也有开销:

合并类型 性能成本 说明
✅ 求和 每个线程只返回一个整数
✅ 收集到 List 常见 Collector 支持高效合并
⚠️ 合并 HashMap 存在键冲突时需要协调、加锁或合并逻辑

示例:合并 HashMap

java 复制代码
Map<Integer, String> map = IntStream.range(0, 10_000)
    .parallel()
    .boxed()
    .collect(Collectors.toMap(
        i -> i % 1000,          // ❗会产生重复 key
        i -> "Val" + i,
        (v1, v2) -> v1 + "/" + v2  // 手动合并冲突值
    ));

💥 如果你忘了写 merge 函数,程序会抛出 IllegalStateException


🚧 并发副作用的地雷区

一旦你引入共享状态,并行流的所有优势都可能崩塌!

java 复制代码
List<Integer> result = new ArrayList<>();

IntStream.range(0, 1000)
         .parallel()
         .forEach(result::add);  // ❌ 并发写入非线程安全集合

☠️ 有副作用的代码不仅慢,而且错得离谱!


📏 判断是否使用并行流的 4 条黄金法则

🧪 法则 内容
Rule #1 不要为了"酷"而优化。只有在确实性能不达标时才考虑并行。
Rule #2 明智地选择数据源。避免使用不能高效拆分的结构(如 LinkedList)。
Rule #3 不要修改外部状态,不要共享可变状态。
Rule #4 不要猜性能!请使用基准测试(如 JMH)来实测

🧪 使用 JMH 快速比较并行与串行性能

java 复制代码
@Benchmark
public void serialSum() {
    IntStream.range(0, 1_000_000).sum();
}

@Benchmark
public void parallelSum() {
    IntStream.range(0, 1_000_000).parallel().sum();
}

📊 有些场景下,串行甚至更快!


✅ 小结语:

并行不是"开个线程就能快"。你要考虑数据结构、任务粒度、副作用、合并成本。否则,你是在用8核CPU完成一个人5秒能搞定的事,还掉进了坑里!

相关推荐
袋鱼不重14 小时前
Hermes Agent 安装与实战:从安装到与 OpenClaw 全方位对比
前端·后端·ai编程
汉秋14 小时前
iOS 自定义 UICollectionView 拼图布局 + 布局切换动画实践
前端
江南月14 小时前
让智能体学会自我改进:从 0 理解 ReflectionAgent 的迭代优化
前端·人工智能
尽欢i14 小时前
前端响应式布局新宠:vw 和 clamp (),你了解吗?
前端·css
写Cpp的小黑黑14 小时前
C++ std::shared_ptr 线程安全性和最佳实践详解
后端
沸点小助手14 小时前
「 AI 整活大赛,正式开擂 & 最近一次面试被问麻了吗」沸点获奖名单公示|本周互动话题上新🎊
前端·人工智能·后端
何陋轩14 小时前
消息队列Kafka与RabbitMQ深度解析:把分布式消息核心讲透,吊打面试官
redis·后端
青Cheng序员石头14 小时前
龙虾运行时安全部署 | NVIDIA NemoClaw 深度研究报告
后端·aigc·nvidia
Oneslide14 小时前
解决 df -h 与 lsblk 显示不一致问题(XFS 文件系统实操记录)
后端
攀登的牵牛花14 小时前
我用 Mac 折腾本地生图一整天,实现了本地文生图自由
前端·llm