相信大家都对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
英文原文链接
想及时了解博客更新,可以关注我的微博
/deepinmind]Java译站
分享到:
相关推荐
在Oracle数据库系统中,"ORA-00060: 等待资源时检测到死锁" 是一个常见的错误提示,它表明两个或多个事务在执行过程中陷入了无法继续进行的状态,因为彼此都在等待对方释放资源。这种情况通常发生在并发操作中,比如...
3. **死锁恢复**:一旦检测到死锁,系统需要选择牺牲一部分事务(通常是回滚)来恢复系统的正常运行,通常会根据事务的重要性和成本来进行决策。 ### 避免死锁 避免死锁策略试图在保守的预防死锁和放任的检测死锁...
- InnoDB存储引擎内置了死锁检测机制,当检测到死锁时,会终止一个事务,并返回错误信息(错误代码1213)。 - 用户可以通过`SHOW ENGINE INNODB STATUS;`命令查看死锁详情,找出死锁的具体事务和涉及的资源。 - ...
当检测到死锁时,它会尝试选择一个受害者事务进行回滚,以打破死锁循环,这通常由DB2的死锁超时设置触发。你可以通过调整DB2的配置参数,如DEADLOCK_TIMEOUT,来定制死锁检测的灵敏度。 为了预防DB2中的死锁,有...
当对于数据库某个表的某一列做更新或删除等操作,执行完毕后该条语句不提 交,另一条对于这一列数据做更新操作的语句在执行的时候就会处于等待状态, 此时的现象是这条语句一直在执行,但一直没有执行成功,也没有...
当Oracle数据库检测到死锁时,会在警报日志文件(alert.log)中记录相关信息。这些信息对于分析死锁的原因至关重要。例如,在提供的部分内容中,可以看到以下日志条目: ``` Mon Aug 07 09:14:42 2007 ORA-000060: ...
Oracle数据库内置了死锁检测机制,当检测到死锁时,会自动选择一个受害者事务进行回滚,释放资源,从而打破死锁。另外,DBA可以通过以下方式手动检查: - 使用`v$session_wait`视图查找等待类型的`ENQ: TX - row ...
- **检测和恢复死锁**:系统定期检查是否存在死锁,并在检测到死锁时,通过回滚事务、剥夺资源或终止部分进程来解除死锁。 除了死锁,我们还涉及到了几种网络拓扑结构: 1. **星形网络**:以中心节点为核心,所有...
在检测到死锁后,我们将输出检测结果,包括是否出现死锁和死锁的进程循环等待队列。 在实现中,我们使用了C++语言,并使用了文件输入/输出操作来读取和写入文件。我们定义了两个结构体:`cell`,用于表示进程和资源...
- 事务回滚:数据库管理系统会自动检测到死锁,并选择一个事务进行回滚以打破死锁。 - 手动干预:管理员可以通过终止特定事务、调整事务的执行顺序或优化SQL语句来避免死锁。 - 设置超时:为事务设置超时限制,超时...
4. 当检测到死锁时,调用`remove()`函数,撤销所有死锁进程,释放它们占用的资源,并重新显示系统状态。 源代码中,`check()`函数遍历进程,检查每个进程是否能获得其请求的全部资源。如果可以,说明进程是安全的;...
一旦检测到死锁,SQL Server默认的处理方式是选择一个受害方(victim),即终止其中一个事务以解除死锁。此外,还可以通过以下方式处理死锁: 1. **配置死锁优先级** - 可以为每个会话设置死锁优先级,优先级高的...
4. **生成报告**:一旦检测到死锁,工具会生成详细的报告,包括涉及的进程、线程、资源和等待链路,帮助用户理解和解决死锁问题。 5. **可视化呈现**:更高级的版本可能还会提供图形界面,用图表形式直观展示死锁...
操作系统中的死锁问题是一个复杂而重要的概念,它涉及到并发进程之间的资源分配和同步问题。死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉它们都将无法推进下去。这个...
- 进程回滚:让死锁进程回退到一个无死锁的状态,然后重新执行。 - 资源分配策略调整:改变资源分配策略,避免再次发生死锁。 五、代码实现 在给定的文件中,"SafetyAutoLock.cpp"和"SafetyAtuoLock.h"可能是实现...
这个概念在计算机科学中尤为重要,因为并发执行是提高系统性能的关键手段,但如果不妥善处理,死锁可能导致整个程序停滞不前。 在我们的例子“ConsoleApplication1”中,很可能包含了一个模拟线程死锁的简单控制台...
死锁是指两个或多个事务在执行过程中因争夺资源而造成的一种僵局状态,若不及时处理,将直接影响到系统的性能和稳定性。本文将围绕“Oracle查询死锁表”的主题展开详细讨论,旨在帮助数据库管理员更好地理解和掌握...
3. **安全序列**:如果能够找到一个序列,使得按照此序列依次完成所有进程,则称系统处于安全状态,不存在死锁。反之,则可能存在死锁。 #### 四、示例代码分析 在给出的部分内容中,可以看到一个用C++实现的检测...
- **死锁检测与解除**:数据库系统如MySQL有内置的死锁检测机制,一旦检测到死锁会主动回滚一个事务以解除死锁。 标签 "源码" 和 "工具" 提示我们可能需要深入到Ibatis的源代码中寻找死锁产生的原因,并且可能需要...