当前位置:Java -> 解开Java的"parallelStream"引起的死锁
并发既是软件开发的福音,也是软件开发的苦恼。通过并行处理提升性能的承诺与错综复杂的挑战并行而行,比如臭名昭著的死锁。死锁,多线程编程世界中的潜在问题,可以使最强大的应用程序陷入瘫痪。它描述了两个或多个线程永远被阻塞,互相等待的情况。
在这篇博文中,我们深入探讨了一个由看似无辜的Java parallelStream
使用触发的真实死锁事件。我们将剖析根本原因并仔细检查线程堆栈跟踪。
想象一个宁静的代码库,利用Java的parallelStream
对集合进行加速处理。然而,随着我们的应用程序变得更加复杂,一个意想不到的隐匿问题浮现出来 — 死锁。曾经的盟友,如今被困在一个似是而非的拥抱中。在这篇博文后面,我们将更仔细地查看一些线程的堆栈跟踪,这些线程中的主角有ForkJoinPool.commonPool-worker-0
和ForkJoinPool.commonPool-worker-1
。
在我们解开这个死锁之谜的旅途中,我们转向了一个强大的故障排除工具:yCrash,它在解开我们的死锁谜团中起到了侦探的作用。这个工具专门分析复杂的Java应用程序,识别性能瓶颈、死锁和其他问题。yCrash使我们能够可视化线程的交互,分析线程转储,并准确找出争用问题的源头。有了这个洞察,我们能够理解死锁的起因并制定解决方案。当我们使用yCrash解决问题时,这个工具向我们提供了一个根本原因分析(RCA)摘要页面,如下图所示:
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
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
对情况的深入调查引导我们使用Java的parallelStream
在集合上进行加速处理。对于那些不熟悉的人来说,parallelStream
是一种旨在通过并行处理提升处理速度的机制。虽然parallelStream
机制承诺通过利用并行性来加速处理,但它也有可能引入死锁等复杂问题。
涉及到死锁的线程都在利用默认的“分支-合并池” — 一种共享资源来进行并行操作。我们的线程无意中但不可避免地成为了这个池中相同资源的竞争者。这个共享池无意间创造了这样的情况,使得这些线程最终竞争资源,导致了死锁。
Stream
而非ParallelStream
在我们解开死锁之谜的旅途中,我们发现了一个改变游戏规则的发现。在我们浏览死锁的错综复杂时,我们意识到在集合上使用parallelStream
不经意地加剧了资源竞争。
我们在集合中使用parallelStream
就像是邀请多个朋友一起使用同一个玩具 — 导致了大量的拥挤和推搡。这种骚动导致我们的线程发生冲突并最终导致死锁。但我们没有放弃; 我们决定玩法不同。
与其让每个人一起玩玩具,我们让他们轮流玩。我们从parallelStream
转换到了更简单的方法——只使用stream
。这个变化意味着一次只有一个朋友在玩玩具。这种友好的轮流玩耍减少了发生争吵的机会,并且使我们的线程能够协同工作而不是相互冲突。
这个转变不只是在我们的代码中改动了一个词;它就像是在游戏中选择了一种新的策略。猜猜看?它奏效了!现在,线程们一个接一个地进行游戏,不再互相碰撞。这意味着不再有死锁,我们的应用程序能松了口气。
下面,您会发现突出这一关键转变的代码片段:
原始代码:
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”转换到普通的stream确实产生了显著的影响。
在未来的道路上,我们牢记最佳解决方案并非总是最复杂的。谨慎的选择将死锁障碍转变为一条更加顺畅的道路,这一切得益于yCrash的深入分析。我们在编码的道路上回响着探索、学习以及做出明智决策的精髓。
推荐阅读: 26.Web开发相关注解
本文链接: 解开Java的"parallelStream"引起的死锁