那些看不见的死锁

相信大家都对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 N o n f a i r S y n c ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . L o c k S u p p o r t . p a r k ( L o c k S u p p o r t . j a v a : 186 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . A b s t r a c t Q u e u e d S y n c h r o n i z e r . p a r k A n d C h e c k I n t e r r u p t ( A b s t r a c t Q u e u e d S y n c h r o n i z e r . j a v a : 834 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . A b s t r a c t Q u e u e d S y n c h r o n i z e r . a c q u i r e Q u e u e d ( A b s t r a c t Q u e u e d S y n c h r o n i z e r . j a v a : 867 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . A b s t r a c t Q u e u e d S y n c h r o n i z e r . a c q u i r e ( A b s t r a c t Q u e u e d S y n c h r o n i z e r . j a v a : 1197 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . R e e n t r a n t R e a d W r i t e L o c k 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 NonfairSync)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:186)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:867)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1197)atjava.util.concurrent.locks.ReentrantReadWriteLockWriteLock.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 W o r k e r . r u n ( T h r e a d P o o l E x e c u t o r . j a v a : 603 ) a t j a v a . l a n g . T h r e a d . r u n ( T h r e a d . j a v a : 722 ) " p o o l − 1 − t h r e a d − 1 " : a t o r g . p h . j a v a e e . t r a i n i n g 8. T a s k . e x e c u t e T a s k 1 ( T a s k . j a v a : 31 ) − w a i t i n g t o l o c k < 0 x 272236 d 0 > ( a j a v a . l a n g . O b j e c t ) a t o r g . p h . j a v a e e . t r a i n i n g 8. W o r k e r T h r e a d 1. r u n ( W o r k e r T h r e a d 1. j a v a : 29 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r . r u n W o r k e r ( T h r e a d P o o l E x e c u t o r . j a v a : 1110 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r 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)atjava.lang.Thread.run(Thread.java:722)"pool1thread1":atorg.ph.javaee.training8.Task.executeTask1(Task.java:31)waitingtolock<0x272236d0>(ajava.lang.Object)atorg.ph.javaee.training8.WorkerThread1.run(WorkerThread1.java:29)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)atjava.util.concurrent.ThreadPoolExecutorWorker.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() 来记录下读锁的数量。
原创文章转载请注明出处:那些看不见的死锁

英文原文链接

想及时了解网站更新,可以关注本站微博Java译站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值