许多程序员都熟悉Java线程死锁的概念。死锁就是两个线程一直相互等待。这种情况通常是由同步或者锁的访问(读或写)不当造成的。
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”
好消息是最新的JVM通常会帮你检测到这种死锁现象,但它真的做到了吗?最近一个线程死锁问题影响了Oracle Service Bus的生产环境,这一消息使得我们不得不重新审视这一经典问题,并找出“隐藏”死锁存在的情况。本文将通过一个简单的Java程序向大家讲解一种非常特殊的锁顺序死锁问题,这种死锁在最新的JVM 1.7中并没有被检测到。文章末尾的视频讲解了这段Java示例代码以及问题的解决方法。
犯罪现场
通常,我习惯将出现严重Java并发问题的情况称之为犯罪现场,在这里你扮演一个侦查员的角色来解决问题。在这篇文章中,犯罪行为来源于客户端IT环境运行中断。你需要完成如下工作:
收集证据、线索和事实(线程转储,日志,业务影响,负载信息…)
审问目击证人、咨询相关领域专家(支撑团队,交付团队,供应商,客户…)
接下来的调查工作为:分析收集到的信息,并根据收集的证据建立一个或多个“嫌疑犯”名单。最终,将名单缩小到主要嫌犯或者说引发问题的根源者上。显然,“凡不能被证明有罪者均无罪”的条例在这里并不适用,这里用到的规则恰恰相反。缺少证据会妨碍你找到问题的根源。下一步你将会看到JVM对死锁检测的缺乏并不能说明你无法解决这一问题。
嫌疑犯
在解决该问题的过程中,“嫌疑犯”被定义为具有以下执行模式的应用程序或中间件代码:
在ReentrantLock写锁使用之后使用普通锁(执行线程#1)
在使用普通锁之后使用ReentrantLock 读锁(执行线程#2)
当前的程序由两个Java线程并发执行,但执行顺序与正常顺序相反
上面的锁排序死锁标准可以用下图表示:
现在我们通过Java实例程序说明这一问题,同时查看JVM线程转储输出。
Java实例程序
上面的死锁问题第一次是在Oracle OSB问题事例中发现的。之后,我们通过实例程序重建了该死锁。你可以从这里下载程序的源码。该程序只是简单的创建了两个线程,每个线程有不同的执行路径,并且以不同的顺序尝试获取共享对象的锁。我们还创建了一个死锁线程用来监控和记录。现在,下面的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;
}
}
一旦程序引起线程死锁,JVM虚拟机就会产生如下的线程转储输出。
死锁根源:ReetrantLock 读锁行为
我们发现在这一问题上主要和ReetrantLock读锁的使用有关。读锁通常不会被设计成具有所有权的概念(详细信息)。由于线程没有记录读锁,造成了HotSpot JVM死锁检测器的逻辑无法检测到涉及读锁的死锁。自发现该问题以后,JVM做了一些改进,但是我们发现JVM仍然不能检测到这种特殊场景下的死锁。现在,如果我们把程序中读锁替换成写锁,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跟踪,这点和普通锁相似。这就意味着JVM死锁检测器能够检测如下情况的死锁:* 对象监视器上涉及到普通锁的死锁* 和写锁相关的涉及到锁定的可同步的死锁
由于线程缺少对读锁的跟踪造成这种场景下JVM无法检测到死锁,这样增加了解决死锁问题的难度。我推荐你读一下Doug Lea关于这个问题的评论。由于一些潜在的死锁会被忽略,在2005年人们再次提出是否有可能增加线程对读锁的跟踪。如果你遇到了涉及读锁的隐藏死锁,试试下面的建议:* 仔细分析线程调用的跟踪堆栈,它可以揭示一些代码可能获取读锁同时防止其他线程获取写锁* 如果你是代码的拥有者,调用lock.getReadLockCount的方法跟踪读锁的计数
非常期待你的反馈,尤其是那些遇到过读锁造成死锁的开发者。最后,看看下面的视频,我们通过执行和监控我们的实例程序说明了本文讨论的问题。