java并发程序死锁检测_Java并发:隐藏的线程死锁

许多程序员都熟悉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线程并发执行,但执行顺序与正常顺序相反

上面的锁排序死锁标准可以用下图表示:T3c1fNilanTfPRov.png

现在我们通过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虚拟机就会产生如下的线程转储输出。51wjqhhOPLsHXuU2.png

死锁根源: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的方法跟踪读锁的计数

非常期待你的反馈,尤其是那些遇到过读锁造成死锁的开发者。最后,看看下面的视频,我们通过执行和监控我们的实例程序说明了本文讨论的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值