虽然防止死锁的主要责任在于你自己,但JVM仍然通过线程转储来帮助识别死锁的发生。线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息。线程转储还包括加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。在生成线程转储之前,JVM将在等待关系图通过循环来找出死锁。如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置。
要在UNIX平台上触发线程转储操作,可以通过向JVM的进程发送SIGQUIT信号(kill -3),或者在UNIX平台中按下Ctrl-\键,在Windows平台中按下Ctrl-Break键。在许多IDE(集成开发环境)中都可以请求线程转储。
如果使用显式的Lock类而不是内部锁,那么Java5.0并不支持与Lock相关的转储信息,在线程转储中不会出现显式地Lock。虽然在Java6中包含了对显式Lock地线程转储和死锁检测等的支持,但在这些锁上获得的信息比在内置锁上获得的信息精确度低。内置锁与获得它们所在的线程栈帧是相关联的,而显式的Lock只与获得它的线程相关联。
如下图片给出了一个J2EE应用程序中获取的部分线程的转储信息。在导致死锁的故障中包括3个组件:一个J2EE应用程序,一个J2EE容器,以及一个JDBC驱动程序,分别由不同的生产商提供。这3个组件都是商业产品,并经过大量的测试,但每一个组件中都存在一个错误,并且这个错误只有当它们进行交互时才会显现出来,并导致服务器出现一个严重的故障。
当诊断死锁时,JVM可以帮我们做许多工作——哪些锁导致了这个问题,设计哪些线程,它们持有哪些其他的锁,以及是否间接地给其他线程带来不利的影响。其中一个线程持有MumbleDBConnection上的锁,并等待获得MumbleDBCallableStatement上的锁,而另一个线程持有MumbleDBCallableStatement上的锁,并等待MumbleDDConnection上的锁。
在这里使用的JDBC驱动程序中明显存在一个锁顺序问题:不同的调用链通过JDBC驱动程序以不同的顺序获取多个锁。如果不是由于另一个错误,这个问题永远不会显现出来:多个线程试图同时使用同一个JDBC连接。这并不是应用程序的设计初衷——开发人员惊讶地发现同一个Connection被两个线程并发使用。在JDBC规范中并没有要求Connection必须是线程安全的,以及Connection通常被封闭在单个线程中使用,而在这里就采用了这种假设。这个生产商视图提供一个线程安全的JDBC驱动,因此在驱动程序代码内部对多个JDBC对象施加了同步机制。然而,生产商却没有考虑锁的顺序,因而驱动程序很容易发生死锁,而正是由于这个存在死锁风险的驱动程序与错误共享Connection的应用程序发生了交互,才使得这个问题暴露出来。因为单个错误并不会产生死锁,只有这两个错误同时发生时才会产生,即使它们分别进行了大量测试。