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

ops/2024/9/29 18:55:45/

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

在这篇博文中,我们深入探讨了现实世界中因使用 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”切换到一种更简单的方式——只需“流式传输”。这一变化意味着一次只有一个朋友玩玩具。这种友好的轮流减少了打架的机会,并使我们的线程一起工作而不会发生冲突。

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

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

原始代码:

java">List<?> elements = …elements.parallelStream().map(e -> {// Some code here that is causing deadlock…}).collect(Collectors.toList());

修订后的代码:

java">List<?> elements = …elements.stream().map(e -> {// Some code here that was causing deadlock…}).collect(Collectors.toList());

带来重大改变

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


http://www.ppmy.cn/ops/118002.html

相关文章

大型语言模型(Large Language Models)的介绍

背景 大型语言模型&#xff08;Large Language Models&#xff0c;简称LLMs&#xff09;是一类先进的人工智能模型&#xff0c;它们通过深度学习技术&#xff0c;特别是神经网络&#xff0c;来理解和生成自然语言。这些模型在自然语言处理&#xff08;NLP&#xff09;领域中扮…

Elasticsearch 中变更索引的方法

Elasticsearch 提供了几种方法来变更索引。以下是一些常用的方法&#xff1a; 1. 更新索引设置 可以使用 Update Index Settings API 来修改部分索引设置。例如: PUT /my-index/_settings {"index" : {"number_of_replicas" : 2} }2. 重新索引数据 使用…

AB plc设备数据 转profinet IO项目案例

目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 准备工作 2 4 网关采集AB PLC数据 2 5 用PROFINET IO协议转发数据 4 6 案例总结 7 1 案例说明 设置网关采集AB PLC数据把采集的数据转成profinet IO协议转发给其他系统。 2 VFBOX网关工作原理 VFBOX网关是协议转换网关&#xff0…

MySQL知识点复习 - 常用的日志类型

MySQL中常用的日志类型&#xff1a; 重做日志&#xff08;redo log&#xff09; 作用&#xff1a;确保事务的持久性。redo日志记录事务执行后的状态&#xff0c;用来恢复还未写入data file的已成功事务更新的数据。防止在发生故障的时间点&#xff0c;尚有脏页未写入磁盘&…

MySQl查询分析工具 EXPLAIN ANALYZE

文章目录 EXPLAIN ANALYZE是什么Iterator 输出内容解读EXPLAIN ANALYZE和EXPLAIN FORMATTREE的区别单个 Iterator 内容解读 案例分析案例1 文件排序案例2 简单的JOIN查询 参考资料&#xff1a;https://hackmysql.com/book-2/ EXPLAIN ANALYZE是什么 EXPLAIN ANALYZE是MySQL8.…

汽车信息安全 -- 存到HSM中的密钥还需包裹吗?

目录 1.车规芯片的ROM_KEY 2.密钥加密与包裹 3.瑞萨RZ\T2M的密钥导入 4.小结 在车控类ECU中&#xff0c;我们通常把主控芯片MCU中的HSM以及HSM固件统一看做整个系统安全架构的信任根。 所以大家默认在HSM内部存储的数据等都是可信的&#xff0c;例如CycurHSM方案中使用HSM…

javascript中什么是事件代理

事件代理&#xff08;Event Delegation&#xff09; 事件代理是指利用 JavaScript 的事件冒泡机制&#xff0c;将子元素的事件委托到父元素来处理&#xff0c;而不是为每个子元素单独绑定事件监听器。通过给父元素绑定一个事件监听器&#xff0c;当子元素触发事件时&#xff0…

IText导出pdf不显示泰文

使用IText导出PDF意外发现其他外文都能导出成功&#xff0c;只有泰文会消失&#xff0c;查了一下没有能用的办法&#xff0c;官网也没有我这种情况&#xff0c;最后还是误打误撞试出来的。还是要下载泰文字体&#xff0c;网上很多&#xff0c;我是从这里下载的&#xff1a;http…