当前位置:Java -> 解开Java的"parallelStream"引起的死锁

解开Java的"parallelStream"引起的死锁

并发既是软件开发的福音,也是软件开发的苦恼。通过并行处理提升性能的承诺与错综复杂的挑战并行而行,比如臭名昭著的死锁。死锁,多线程编程世界中的潜在问题,可以使最强大的应用程序陷入瘫痪。它描述了两个或多个线程永远被阻塞,互相等待的情况。

在这篇博文中,我们深入探讨了一个由看似无辜的Java parallelStream使用触发的真实死锁事件。我们将剖析根本原因并仔细检查线程堆栈跟踪。

场景

想象一个宁静的代码库,利用Java的parallelStream对集合进行加速处理。然而,随着我们的应用程序变得更加复杂,一个意想不到的隐匿问题浮现出来 — 死锁。曾经的盟友,如今被困在一个似是而非的拥抱中。在这篇博文后面,我们将更仔细地查看一些线程的堆栈跟踪,这些线程中的主角有ForkJoinPool.commonPool-worker-0ForkJoinPool.commonPool-worker-1

利用 yCrash 解决问题

在我们解开这个死锁之谜的旅途中,我们转向了一个强大的故障排除工具:yCrash,它在解开我们的死锁谜团中起到了侦探的作用。这个工具专门分析复杂的Java应用程序,识别性能瓶颈、死锁和其他问题。yCrash使我们能够可视化线程的交互,分析线程转储,并准确找出争用问题的源头。有了这个洞察,我们能够理解死锁的起因并制定解决方案。当我们使用yCrash解决问题时,这个工具向我们提供了一个根本原因分析(RCA)摘要页面,如下图所示:

fig: Image from RCA Report fig: Image from RCA Report
在“应用程序中的问题”部分回顾后,我们发现该工具准确地指出了应用程序中的死锁。当我们点击“这里是线程”的超链接时,该工具显示了以下两个线程的堆栈跟踪:

线程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

对情况的深入调查引导我们使用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"引起的死锁