那些看不见的死锁


相信大家都对Java线程死锁的概念并不陌生。本质上就是有两个线程在互相等待。这通常都是flat锁(synchronized)或者 ReentrantLock的锁排列引起的问题。


Found one Java-level deadlock:

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

"pool-1-thread-2":

  waiting to lock monitor 0x0237ada4 (object 0x272200e8, a java.lang.Object),

  which is held by "pool-1-thread-1"

"pool-1-thread-1":

  waiting to lock monitor 0x0237aa64 (object 0x272200f0, a java.lang.Object),

  which is held by "pool-1-thread-2"



还好就是HotSpot JVM通常都能帮你检测到这样的问题,但也不一定。最近一个死锁问题影响到了生产环境上的Oracle Service Bus(OSB),让我们有必要重新认识下这个经典的问题了,我们得找出那些隐藏的死锁。本文将通过一个简单的Java程序和一组特殊的锁顺序来演示一个连最新的HotSpot 1.7 JVM也无法检测到的死锁的现场。本文末后有个小视频,它将告诉你如何使用这个小程序来重现这一场景。

犯罪现场



我喜欢将严重的Java并发问题比作犯罪现场,因为在这里你就像一个探长一样。你的生产环境的故障就像是一次犯罪纪录。而你工作就是:


  • 收集证据,线索(thread dump,日志,业务影响,加载的配置等)
  • 询问受害人以及领域专家(比如支持团队,发布团队,供应商,客户等)


调查的下一步就是分析收集来的信息,通过切实的证据,建立一个嫌疑人列表。最后你需要缩小范围,定位出头号嫌疑犯。很明显,法律上讲的“无罪推断”在这里并不适用,我们甚至还反其道而行之。没有足够的证据你就没法完成上述的目标。下面你会看到,虽然HotSpot JVM无法检测出死锁,但这并不说明我们就对此束手无策了。

嫌疑人



从故障诊断上下文能看出,应用或者中间件的这段代码的运行模式有问题,它就是嫌犯。


  • 获取了flat锁后,紧接着又去获取ReentrantLock的写锁(执行路径1)
  • 先获取了ReentrantLock的读锁,然后去获取flat锁(执行路径2)
  • 两个线程并发的执行,却执行顺序恰恰相反


上述的死锁排列条件可以用下图来更清楚的说明:



现在我们通过一个Java程序来重现这个场景,然后看下JVM输出的thread dump。

示例程序



上述的死锁条件是从我们的Oracle OSB服务的出现的问题中发现的。然后我们通过一段Java程序重现了它。从这你可以下载到我们程序完整的代码。这个程序其实就是创建了两个工作线程。每个线程执行不同的执行路径,并通过相反的顺序来获取共享对象上的锁。我们也创建了一个死锁检测线程来监控和纪录日志。现在,看下这两条执行路径的Java程序吧。


package org.ph.javaee.training8;



import java.util.concurrent.locks.ReentrantReadWriteLock;



/**

 * A simple thread task representation

 * @author Pierre-Hugues Charbonneau

 *

 */

public class Task {

      

       // Object used for FLAT lock

       private final Object sharedObject = new Object();

       // ReentrantReadWriteLock used for WRITE & READ locks

       private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

      

       /**

        *  Execution pattern #1

        */

       public void executeTask1() {

            

             // 1. Attempt to acquire a ReentrantReadWriteLock READ lock

             lock.readLock().lock();

            

             // Wait 2 seconds to simulate some work...

             try { Thread.sleep(2000);}catch (Throwable any) {}

            

             try {              

                    // 2. Attempt to acquire a Flat lock...

                    synchronized (sharedObject) {}

             }

             // Remove the READ lock

             finally {

                    lock.readLock().unlock();

             }           

             System.out.println("executeTask1() :: Work Done!");

       }

      

       /**

        *  Execution pattern #2

        */

       public void executeTask2() {

             // 1. Attempt to acquire a Flat lock

             synchronized (sharedObject) {                 

                   

                    // Wait 2 seconds to simulate some work...

                    try { Thread.sleep(2000);}catch (Throwable any) {}

                   

                    // 2. Attempt to acquire a WRITE lock                   

                    lock.writeLock().lock();

                   

                    try {

                           // Do nothing

                    }

                   

                    // Remove the WRITE lock

                    finally {

                           lock.writeLock().unlock();

                    }

             }

             System.out.println("executeTask2() :: Work Done!");

       }

      

       public ReentrantReadWriteLock getReentrantReadWriteLock() {

             return lock;

       }

}



死锁条件被触发的时候,我们也通过JVisualVM生成了一份JVM的thread dump文件。



从上面的thread dump可以看到,JVM并没有检测出这个死锁条件(它并没有提示发现了Java程序的死锁),不过很明显,这两个线程就是处于死锁的状态。

根本原因:ReentrantLock读锁的行为



目前我们发现的最主要的原因就是使用了 ReentrantLock的读锁。读锁的设计中并没有关于持有锁的概念(译注:也就是,你不知道哪个线程持有读锁了)。那么由于没有记录表明某个线程持有读锁,HotSpot JVM的死锁检测程序也无从得知发生了死锁现象。JVM在死锁检测方面已经改进不少了,不过我们发现像这样的特殊的死锁现象它还是检测不了。如果我们把执行路径2中的读锁换成了写锁,JVM就能够发现产生死锁了,这是为什么呢?


Found one Java-level deadlock:

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

"pool-1-thread-2":

  waiting for ownable synchronizer 0x272239c0, (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync),

  which is held by "pool-1-thread-1"

"pool-1-thread-1":

  waiting to lock monitor 0x025cad3c (object 0x272236d0, a java.lang.Object),

  which is held by "pool-1-thread-2"



Java stack information for the threads listed above:

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

"pool-1-thread-2":

       at sun.misc.Unsafe.park(Native Method)

       - parking to wait for  <0x272239c0> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)

       at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

acquireQueued(AbstractQueuedSynchronizer.java:867)

       at java.util.concurrent.locks.AbstractQueuedSynchronizer.

acquire(AbstractQueuedSynchronizer.java:1197)

       at java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.lock(ReentrantReadWriteLock.java:945)

       at org.ph.javaee.training8.Task.executeTask2(Task.java:54)

       - locked <0x272236d0> (a java.lang.Object)

       at org.ph.javaee.training8.WorkerThread2.run(WorkerThread2.java:29)

       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)

       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)

       at java.lang.Thread.run(Thread.java:722)

"pool-1-thread-1":

       at org.ph.javaee.training8.Task.executeTask1(Task.java:31)

       - waiting to lock <0x272236d0> (a java.lang.Object)

       at org.ph.javaee.training8.WorkerThread1.run(WorkerThread1.java:29)

       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)

       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)

       at java.lang.Thread.run(Thread.java:722)




这是因为JVM会像纪录flat锁那样把写锁记录下来。这说明了HotSpot JVM死锁检测器目前的设计是为了检测以下现象的:

  • 对象监视器上flat锁产生的死锁。
  • Locked ownable synchronizers中包含写锁造成的死锁。



在这个场景中,由于没有记录线程使用的读锁,因此检测不出死锁,这会让问题更得相当棘手。我建议你读下Doug Lea对这整个问题的评论,他早在2005年就提出了如果添加线程对读锁的跟踪可能会再来潜在的开销(译注:这或者就是为什么JVM到现在也没有对读锁进行跟踪的原因,Doug Lea的影响力可是非常大的)。 如果你使用了读锁并怀疑程序因此产生了死锁,我建议:



  • 密切分析线程调用栈,你会发现有的线程可能获取了读锁,导致另的线程无法获取写锁。

  • 如果你是代码的owner,通过 lock.getReadLockCount() 来记录下读锁的数量。





原创文章转载请注明出处: http://it.deepinmind.com

英文原文链接

想及时了解博客更新,可以关注我的微博 Java译站



<think> 首先,用户的问题是:“我想查找查看线程死锁的方法 请问查看线程死锁的方法”。这基于之前的引用,我需要参考那些引用提供答案。 系统级指令: - 所有行内数学表达式使用$...$格式 - 独立公式使用$$...$$并单独成段 - LaTeX语法正确 - 使用中文回答 - 生成相关问题 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1] - 用户可见层:回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠 用户的问题是关于“查看线程死锁的方法”,所以我需要从引用中提取信息: - 引用[1]提到:检测死锁的三种方式:jconsole.exe, jvisualvm.exe, jmc.exe - 引用[2]详细描述了jconsole.exe和jvisualvm.exe的用法 - 引用[3]给出了一个Java线程死锁查看分析方法的例子 我需要构建一个结构清晰的回答: 1. **介绍线程死锁的概念**:简要解释什么是线程死锁,以便用户理解为什么需要检测。 2. **逐步列出查看线程死锁的方法**: - 使用工具如jconsole, jvisualvm, jmc - 代码中主动检测(如引用[1]提到的使用Thread.getAllStackTraces()) 3. **提供示例或细节**:基于引用,给出具体的步骤或代码片段。 4. **自然添加引用标识**:在相关部分末尾添加。 5. **生成相关问题**:在回答结束后,添加“相关问题”部分。 回答结构: - 先简要介绍线程死锁检测的重要性。 - 然后分点列出方法: - 使用Java工具:jconsole, jvisualvm, jmc - 代码级检测:使用Thread.getAllStackTraces() - 为每个方法提供简单描述和步骤。 - 确保使用中文。 最后,生成相关问题: - 例如:如何预防线程死锁死锁检测的原理是什么?等等。 由于没有数学表达式,不需要使用LaTeX格式,但如果有变量等,我会用$...$。 现在,草拟回答: 1. **介绍**:线程死锁是多线程编程中的常见问题,检测死锁是调试的关键步骤。以下是几种查看线程死锁的方法。 2. **方法1:使用Java内置工具** - jconsole.exe:进入JDK的bin目录,运行jconsole,连接到Java进程,查看线程死锁。[^2] - jvisualvm.exe:同样在bin目录下运行jvisualvm,检测死锁并提供详细信息,如具体代码行和锁地址。[^2] - jmc.exe:Java Mission Control,提供高级监控和诊断。[^2] 3. **方法2:代码中主动检测** - 使用Thread.getAllStackTraces()获取所有线程堆栈信息,分析是否有线程相互等待锁。[^1] - 示例代码:...(简要描述) 4. **总结**:推荐使用工具进行实时检测。 5. **相关问题**:列出2-3个相关问题。 生成相关问题: 1. 如何预防线程死锁的发生? 2. 死锁检测的工具有哪些优缺点? 3. 在分布式系统中如何检测死锁? 确保引用正确: - 引用[1]:代码检测方法 - 引用[2]:工具检测方法 - 引用[3]:提供分析示例 在回答中,自然地添加引用标识在段落末尾。 </think>### 查看线程死锁的方法 在多线程编程中,线程死锁是常见问题,它发生在多个线程相互等待对方释放锁时,导致系统停滞。检测死锁是调试和优化的关键步骤。以下是几种有效的查看线程死锁的方法,基于Java环境(其他语言可类比)。我将逐步介绍每种方法,确保内容清晰可靠。 #### 1. **使用Java内置工具检测** 这些工具无需修改代码,直接通过JDK提供的实用程序实时监控和分析死锁。方法如下: - **jconsole.exe**: - 进入JDK安装目录下的`bin`文件夹(例如`C:\Program Files\Java\jdk1.8.0_192\bin`)。 - 运行`jconsole.exe`,选择目标Java进程连接。 - 切换到"线程"标签页,系统会自动标记死锁线程,并显示具体代码行号,帮助定位问题。[^2] - **jvisualvm.exe**: - 同样在JDK的`bin`目录下运行`jvisualvm.exe`。 - 连接到Java进程后,进入"线程"视图;如果检测到死锁,点击"Dump"按钮生成详细报告。 - 报告包括:线程名、死锁发生的代码行、等待锁的内存地址和持有锁的内存地址,便于深入分析。[^2] - **jmc.exe (Java Mission Control)**: - 运行`jmc.exe`(位于JDK的`bin`目录),提供更高级的监控功能。 - 在"线程"面板中,死锁线程会被高亮显示,并附带堆栈跟踪信息,适合复杂系统的诊断。[^2] 这些工具操作简单,推荐在开发或测试阶段使用,能快速可视化死锁情况。 #### 2. **代码中主动检测** 如果需要在应用程序运行时动态监控死锁,可以通过编写自定义逻辑实现。例如,使用Java的`Thread.getAllStackTraces()`方法分析线程状态: - **原理**:定期检查所有线程的堆栈跟踪,识别是否有线程相互等待对方持有的锁(例如,线程A等待线程B的锁,同时线程B等待线程A的锁)。 - **示例代码**: ```java import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.lang.management.ManagementFactory; public class DeadlockDetector { public static void detectDeadlock() { ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); long[] threadIds = threadBean.findDeadlockedThreads(); // 获取死锁线程ID if (threadIds != null) { ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds); for (ThreadInfo info : threadInfos) { System.out.println("死锁线程: " + info.getThreadName()); System.out.println("等待锁: " + info.getLockOwnerName() + ",堆栈跟踪: " + info.getStackTrace()); } } } } ``` - 在代码中定期调用`detectDeadlock()`方法(例如,通过定时任务),输出死锁线程的详细信息,包括线程名和等待的锁资源。[^1] - **优点**:适用于生产环境,提供灵活的自定义分析;缺点是需要额外编码。分析时,注意线程如`Thread-1`等待`java.lang.Object@35b4e829`资源,而`Thread-0`等待`java.lang.Object@2db8dc9`资源,形成僵持。[^3] #### 总结 - **推荐流程**:优先使用工具如`jvisualvm`进行快速检测;如需深度集成,采用代码级监控。 - **最佳实践**:在开发阶段定期运行检测,避免死锁影响系统稳定性。死锁的根本原因常是资源竞争,确保锁的获取顺序一致可减少风险。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值