Java中死锁的避免与诊断

文章讨论了如何防止和检测程序中的死锁问题。主要策略包括限制获取锁的数量以避免锁顺序死锁,使用两阶段策略来分析和确保锁的获取顺序,利用定时锁来检测和从死锁中恢复,以及通过线程转储信息来分析死锁。文章强调了防止死锁的重要性,并通过一个J2EE应用程序的死锁例子说明了死锁的复杂性和诊断方法。
摘要由CSDN通过智能技术生成

如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。当然,这种情况通常并不现实,但如果能够避免这种情况,那么就能省去很多工作。如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。

在使用细粒度锁的程序中,可以通过使用一种两阶段策略(Two-Part Strategy)来检查代码中的死锁:首先,找出在什么地方将获取多个锁(使这个集合尽量小),然后对所有这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序都保持一致。尽可能地使用开放调用,这能极大地简化分析过程。如果所有的调用都是开放调用,那么要发现获取多个锁的实例是非常简单的,可以通过代码审查,或者借助自动化的源代码分析工具。

 支持定时的锁

还有一项技术可以检测死锁和从死锁中恢复过来,即显式使用Lock 类中的定时tryLock功能(参见第13章)来代替内置锁机制。当使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限(Timeout),在等待超过该时间后tryLock会返回一个失败信息。如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外情况后重新获得控制权。(在程序清单13-3 中给出了transferMoney的另一种实现,其中使用了一种轮询的tryLock消除了死锁发生的可能性。)

当定时锁失败时,你并不需要知道失败的原因。或许是因为发生了死锁,或许某个线程在持有锁时错误地进入了无限循环,还可能是某个操作的执行时间远远超过了你的预期。然而,至少你能记录所发生的失败,以及关于这次操作的其他有用信息,并通过一种更平缓的方式来重新启动计算,而不是关闭整个进程。

即使在整个系统中没有始终使用定时锁,使用定时锁来获取多个锁也能有效地应对死锁问题。如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后再次尝试,从而消除了死锁发生的条件,使程序恢复过来。(这项技术只有在同时获取两个锁时才有效,如果在嵌套的方法调用中请求多个锁,那么即使你知道已经持有了外层的锁,也无法释放它。)

  通过线程转储信息来分析死锁

虽然防止死锁的主要责任在于你自己,但JVM 仍然通过线程转储(Thread Dump)来帮助识别死锁的发生。线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息。线程转储还包含加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。在生成线程转储之前,JVM 将在等待关系图中通过

                  

即使没有死锁,这些信息对于调试来说也是有用的。通过定期触发线程转储,可以观察程序的加锁行为。

搜索循环来找出死锁。如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置。

要在UNIX平台上触发线程转储操作,可以通过向JVM的进程发送SIGQUIT 信号(kill-3),或者在UNIX 平台中按下Ctrl-\键,在Windows平台中按下Ctrl-Break键。在许多IDE (集成开发环境)中都可以请求线程转储。

如果使用显式的Lock类而不是内部锁,那么Java 5.0并不支持与Lock 相关的转储信息,在线程转储中不会出现显式的Lock。虽然Java 6 中包含对显式Lock 的线程转储和死锁检测等的支持,但在这些锁上获得的信息比在内置锁上获得的信息精确度低。内置锁与获得它们所在的线程栈帧是相关联的,而显式的Lock只与获得它的线程相关联。

程序清单10-7给出了一个J2EE 应用程序中获取的部分线程转储信息。在导致死锁的故障中包括3个组件:一个J2EE应用程序,一个J2EE容器,以及一个JDBC驱动程序,分别由不同的生产商提供。这3个组件都是商业产品,并经过了大量的测试,但每一个组件中都存在一个错误,并且这个错误只有当它们进行交互时才会显现出来,并导致服务器出现一个严重的故障。

                        程序清单10-7   在发生死锁后的部分线程转储信息                           

Found one Java-level deadlock:

========================

"ApplicationServerThread":

waiting to lock monitor 0x080f0cdc (a MumbleDBConnection),which is held by "ApplicationServerThread"

"ApplicationServerThread":

waiting to lock monitor 0x080f0ed4 (a MumbleDBCallableStatement),

which is held by "ApplicationServerThread"

Java stack information for the threads listed above:

"ApplicationServerThread":

at MumbleDBConnection. remove   statement

-waiting to lock <0..650f7f30>(a MumbleDBConnection)

at MumbleDBStatement. close

-locked <0x6024ffb0>(a MumbleDBCallableStatement)

"ApplicationServerThread":

at MumbleDBCallableStatement. sendBatch

-waiting to lock <0x6024ffb0>(a MumbleDBCallableStatement)

at MumbleDBConnection. commit

-  locked   <0x650f7f30>  ( a  MumbleDBConnection)

                                                                       

我们只给出了与查找死锁相关的部分线程转储信息。当诊断死锁时,JVM可以帮我们做许多工作——哪些锁导致了这个问题,涉及哪些线程,它们持有哪些其他的锁,以及是否间接地给其他线程带来了不利影响。其中一个线程持有MumbleDBConnection 上的锁,并等待获得MumbleDBCallableStatement上的锁,而另一个线程则持有MumbleDBCallableStatement上的锁,并等待MumbleDBConnection上的锁。

在这里使用的JDBC 驱动程序中明显存在一个锁顺序问题:不同的调用链通过JDBC驱动程序以不同的顺序获取多个锁。如果不是由于另一个错误,这个问题永远不会显现出来:多个线程试图同时使用同一个JDBC连接。这并不是应用程序的设计初衷——开发人员惊讶地发现同一个Connection被两个线程并发使用。在JDBC规范中并没有要求Connection必须是线程安全的,以及Connection通常被封闭在单个线程中使用,而在这里就采用了这种假设。这个生产商试图提供一个线程安全的JDBC驱动,因此在驱动程序代码内部对多个JDBC对象施加了同步机制。然而,生产商却没有考虑锁的顺序,因而驱动程序很容易发生死锁,而正是由于这个存在死锁风险的驱动程序与错误共享Connection的应用程序发生了交互,才使得这个问题暴露出来。因为单个错误并不会产生死锁,只有这两个错误同时发生时才会产生,即使它们分别进行了大量测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

向前齐步走

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值