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示例代码以及问题的解决方法。


http://javaeesupportpatterns.blogspot.com/2012/07/oracle-service-bus-stuck-thread-case.html


犯罪现场


通常,我习惯将出现严重Java并发问题的情况称之为犯罪现场,在这里你扮演一个侦查员的角色来解决问题。在这篇文章中,犯罪行为来源于客户端IT环境运行中断。你需要完成如下工作:


  • 收集证据、线索和事实(线程转储,日志,业务影响,负载信息…)

  • 审问目击证人、咨询相关领域专家(支撑团队,交付团队,供应商,客户…)


接下来的调查工作为:分析收集到的信息,并根据收集的证据建立一个或多个“嫌疑犯”名单。最终,将名单缩小到主要嫌犯或者说引发问题的根源者上。显然,“凡不能被证明有罪者均无罪”的条例在这里并不适用,这里用到的规则恰恰相反。缺少证据会妨碍你找到问题的根源。下一步你将会看到JVM对死锁检测的缺乏并不能说明你无法解决这一问题。


嫌疑犯


在解决该问题的过程中,“嫌疑犯”被定义为具有以下执行模式的应用程序或中间件代码:


  • 在ReentrantLock写锁使用之后使用普通锁(执行线程#1)

  • 在使用普通锁之后使用ReentrantLock 读锁(执行线程#2)

  • 当前的程序由两个Java线程并发执行,但执行顺序与正常顺序相反


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




现在我们通过Java实例程序说明这一问题,同时查看JVM线程转储输出。


Java实例程序


上面的死锁问题第一次是在Oracle OSB问题事例中发现的。之后,我们通过实例程序重建了该死锁。你可以从这里下载程序的源码。该程序只是简单的创建了两个线程,每个线程有不同的执行路径,并且以不同的顺序尝试获取共享对象的锁。我们还创建了一个死锁线程用来监控和记录。现在,下面的java类中实现了两个不同的执行路径。


https://docs.google.com/file/d/0B6UjfNcYT7yGbmllUEVEM2dFWTQ/edit


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


http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6207928


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


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

发布了41 篇原创文章 · 获赞 157 · 访问量 50万+

猜你喜欢

转载自blog.csdn.net/w05980598/article/details/79928737
今日推荐