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线程并发执行,但执行顺序与正常顺序相反

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

现在我们通过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的方法跟踪读锁的计数

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

观看视频请自备扶梯:Java concurrency: the hidden thread deadlocks

原文链接: javacodegeeks 翻译: ImportNew.com - 人晓
译文链接: http://www.importnew.com/10661.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 人晓

(新浪微博:@人晓

查看人晓的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

2 条评论

  1. kent kwan 说道:

    其实不太深入的理解 反而不会造成误导
    如果归纳为锁顺序死锁问题 只要把ReentrantReadWriteLock 看成一个独立的锁对象 那么 shareObject和lock的顺序不一致 必然产生死锁

    Thumb up 0 Thumb down 0

    • XuNeal 说道:

      作者想强调的是“使用读锁的话JVM不能判断出程序是否死锁”这个概念吧。

      Thumb up 0 Thumb down 0

跳到底部
返回顶部