那些看不见的死锁

本文详细解析了Java线程死锁的原理及其检测难点,特别是当ReentrantLock读锁与flat锁交互时导致的死锁问题。通过一个简单的Java程序实例,展示了如何重现这一经典死锁场景,并揭示了JVM在特定情况下无法检测出死锁的原因。此外,文章还提供了定位和解决这类问题的方法,包括分析线程调用栈和记录读锁数量的建议。
部署运行你感兴趣的模型镜像
相信大家都对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也无法检测到的死锁的现场。本文末后有个小视频,它将告诉你如何使用这个小程序来重现这一场景。

[b]犯罪现场[/b]

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

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

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

[b]嫌疑人[/b]

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

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

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

[img]http://www.javacodegeeks.com/wp-content/uploads/2013/02/Deadlock_Flat_Reentrant_locks.png[/img]


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

[b]示例程序[/b]

上述的死锁条件是从我们的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文件。

[img]http://www.javacodegeeks.com/wp-content/uploads/2013/02/Hidden_deadlock_sample_Java_program.png[/img]

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

[b]根本原因:ReentrantLock读锁的行为[/b]

目前我们发现的最主要的原因就是使用了 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死锁检测器目前的设计是为了检测以下现象的:
[list]
[*]对象监视器上flat锁产生的死锁。
[*] Locked ownable synchronizers中包含写锁造成的死锁。
[/list]


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

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

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


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

[url=http://it.deepinmind.com]英文原文链接[/url]

想及时了解博客更新,可以关注我的微博[url=http://weibo.com]/deepinmind]Java译站[/url]

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

<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`进行快速检测;如需深度集成,采用代码级监控。 - **最佳实践**:在开发阶段定期运行检测,避免死锁影响系统稳定性。死锁的根本原因常是资源竞争,确保的获取顺序一致可减少风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值