当前位置: 首页 > article >正文

解决 Java 中由于 parallelStream 导致的死锁

并发性是软件开发的福音,也是祸根。通过并行处理提高性能的承诺与错综复杂的挑战相伴而生,例如臭名昭著的死锁。死锁是多线程编程世界中的隐患,它甚至可以使最强大的应用程序陷入瘫痪。它描述了两个或多个线程永远被阻塞,相互等待的情况。

在这篇博文中,我们深入探讨了现实世界中因使用 Java 的“ parallelStream ”而引发的死锁事件。我们将剖析根本原因,仔细检查线程堆栈跟踪。

场景

想象一下一个平静的代码库,它利用 Java 的“parallelStream”对 Collection 进行处理以提高处理速度。然而,随着我们的应用程序变得越来越复杂,出现了一个意想不到的隐蔽问题——死锁。线程曾经是盟友,现在却陷入了矛盾的境地。在这篇文章的后面,我们将仔细研究一些线程的堆栈跟踪,其中的主角是“ForkJoinPool.commonPool-worker-0”和“ForkJoinPool.commonPool-worker-1”。

以下两个线程堆栈跟踪:

线程 1:ForkJoinPool.commonPool-worker-0

stackTrace:

java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.app.DataParser.read(DataParser.java:58)
- waiting to lock <0x00000001d6ff09f0> (a com.example.app.DataParser)
at com.example.app.ObjectLoader.read(ObjectLoader.java:196)
at com.example.app.MemorySnapshot$ObjectCacheManager.load(MemorySnapshot.java:2152)
at com.example.app.MemorySnapshot$ObjectCacheManager.load(MemorySnapshot.java:1)
at com.example.app.ObjectCache.get(ObjectCache.java:52)
- locked <0x00000001d6fafb00> (a com.example.app.MemorySnapshot$ObjectCacheManager)
at com.example.app.MemorySnapshot.getObject(MemorySnapshot.java:1453)
:
:
:
at com.example.app.AnalyzerImpl.lambda$37(AnalyzerImpl.java:3248)
at com.example.app.AnalyzerImpl$$Lambda$150/179364589.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:747)
at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:721)
at java.util.stream.AbstractTask.compute(AbstractTask.java:327)
at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)
Locked ownable synchronizers:
- None

线程2:ForkJoinPool.commonPool-worker-1

stackTrace:

java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.app.ObjectCache.get(ObjectCache.java:44)
- waiting to lock <0x00000001d6fafb00> (a com.example.app.MemorySnapshot$ObjectCacheManager)
at com.example.app.MemorySnapshot.getObject(MemorySnapshot.java:1453)
at com.example.app.DataParser.readObjectArrayDump(DataParser.java:135)
at com.example.app.DataParser.read(DataParser.java:65)
- locked <0x00000001d6ff09f0> (a com.example.app.DataParser)
at com.example.app.ObjectLoader.read(ObjectLoader.java:196)
at com.example.app.ObjectInstance.read(ObjectInstance.java:135)
- locked <0x00000001e5822bf8> (a com.example.app.ObjectInstance)
at com.example.app.ObjectInstance.getAllFields(ObjectInstance.java:101)
:
:
:
at com.example.app.AnalyzerImpl.lambda$37(AnalyzerImpl.java:3248)
at com.example.app.AnalyzerImpl$$Lambda$150/179364589.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:747)
at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:721)
at java.util.stream.AbstractTask.compute(AbstractTask.java:327)
at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)
Locked ownable synchronizers:
- None

线程堆栈跟踪让我们得以一窥死锁的核心。有趣的是,两个线程都纠缠在同一个方法中 — — 这清楚地表明它们正在争夺一个共享资源。有问题的资源是一个对象监视器,它通常是死锁场景中的罪魁祸首。

进一步分析表明,这些线程正在尝试执行需要独占访问“ com.example.app ”包内的对象缓存的操作。这种对资源访问的竞争是导致死锁的根本原因。这种资源争用构成了死锁的核心,每个线程都在等待对方放弃控制权。令人惊讶的是,这场灾难的罪魁祸首是人们所熟悉的“ parallelStream ”……

罪魁祸首:parallelStream

更深入地研究这种情况,我们可以在 Collection 上使用 Java 的parallelStream* 。对于不熟悉的人, “parallelStream”是一种旨在通过利用并行性来提高处理速度的机制。虽然“parallelStream”机制有望通过利用并行性来加速处理,但它可能会带来复杂情况,包括死锁。*

陷入死锁的线程都使用了默认的“fork-join 池”——一种促进并行操作的共享资源。我们的线程不知不觉地但不可避免地成为了该池中同一资源的竞争对手。这个共享池无意中造成了一种情况,即线程最终争夺资源,从而导致死锁。

更顺畅的道路:选择 Stream 而不是 ParallelStream

在解开死锁难题的过程中,我们发现了改变游戏规则的因素。在解开错综复杂的死锁网络时,我们意识到集合上使用 parallelStream 无意中加剧了资源争用。

我们在集合中使用 parallelStream 就像邀请多个朋友同时使用同一个玩具一样——这会导致很多推搡。这种骚动导致我们的线程发生冲突并导致死锁。但我们没有放弃;我们决定换一种玩法。

我们没有让所有人一起玩玩具,而是让他们轮流玩。我们从“parallelStream”切换到一种更简单的方式——只需“流式传输”。这一变化意味着一次只有一个朋友玩玩具。这种友好的轮流减少了打架的机会,并使我们的线程一起工作而不会发生冲突。

这种切换不仅仅是改变代码中的一个单词;它就像在游戏中选择一种新策略。猜猜怎么着?它成功了!线程现在一个接一个地运行,不再相互碰撞。这意味着不再有死锁,我们的应用程序可以松一口气了。

下面,您将看到突出显示这一关键转变的代码片段:

原始代码:

List<?> elements = …

elements.parallelStream().map(e -> {

   // Some code here that is causing deadlock…

}).collect(Collectors.toList());

修订后的代码:

List<?> elements = …

elements.stream().map(e -> {

   // Some code here that was causing deadlock…

}).collect(Collectors.toList());

带来重大改变

我们的死锁挑战表明,小的改变可以产生巨大的效果。从“parallelStream”切换到常规流虽然简单,但却带来了显著的变化。


http://www.kler.cn/news/324404.html

相关文章:

  • BUG项目管理
  • 【MAC】安装realsense
  • HttpServletRequest简介
  • 美团中间件C++一面-面经总结
  • 25维谛技术面试最常见问题面试经验分享总结(包含一二三面题目+答案)
  • 基于两分支卷积和 Transformer 的轻量级多尺度特征融合超分辨率网络 !
  • 如何组织一场考试并筛选未参加答题的考生?
  • 搜索:如何用 A*搜索算法实现游戏中的寻路功能?
  • Python脚本示例,你可以使用这个脚本来自动化登录网站、选择页面元素和提交表单
  • 『网络游戏』GoLand服务器框架【01】
  • 第 2 章:Vue 组件化编程
  • Cluster Explanation via Polyhedral Descriptions
  • 【性能优化】低配starRocks常驻内存优化
  • py-mmcif提取结构解析的方法、提交日期以及pdb ID等
  • 基于Node.js+Express+MySQL+VUE实现的在线电影视频点播网站管理系统的设计与实现部署安装
  • Kubernetes服务发布基础
  • [leetcode刷题]面试经典150题之9python哈希表详解(知识点+题合集)
  • [go] 状态模式
  • CSS3 字体
  • 卷轴模式开发的技术架构分析与源代码展示
  • 数据结构讲解二叉树 【一】
  • 自动化测试实例:Web登录功能性测试(无验证码)
  • 【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
  • S32K312 RTD 4.0.0 版本 OCU 例程配置流程说明
  • HuggingChat macOS 版现已发布
  • 【路径规划】基于球向量的粒子群优化(SPSO)算法在无人机路径规划中的实现
  • 002.动手实现softmax回归(pytorch简洁版)
  • AutosarMCAL开发——基于EB MCU驱动
  • 爬虫逆向学习(八):Canvas画图滑块验证码解决思路与绕过骚操作
  • 第十四章:html和css做一个心在跳动,为你而动的表白动画