大多数Java程序员熟悉Java线程死锁概念。 它本质上涉及2个线程,它们彼此永远等待。 这种情况通常是平面(同步)或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生产环境的线程死锁问题迫使我们重新审视此经典问题并确定是否存在“隐藏”死锁情况。 本文将通过一个简单的Java程序演示并复制非常特殊的锁排序死锁条件,最新的HotSpot JVM 1.7并未检测到该情况。 您还可以在本文结尾处找到一个视频 ,向您介绍Java示例程序和所使用的故障排除方法。
犯罪现场
我通常喜欢将主要的Java并发问题与犯罪现场进行比较,在犯罪现场您扮演首席调查员的角色。 在这种情况下,“犯罪”是客户IT环境的实际生产中断。 您的工作是:
- 收集所有证据,提示和事实(线程转储,日志,业务影响,负载数字…)
- 询问证人和领域专家(支持团队,交付团队,供应商,客户……)
调查的下一步是分析收集的信息,并建立一个或多个“可疑”的潜在清单以及清晰的证据。 最终,您希望将其范围缩小到主要可疑或根本原因。 显然,“直到证明有罪之前无罪”的法律在这里并不适用,恰恰相反。 缺乏证据可能会阻止您实现上述目标。 接下来,您将看到Hotspot JVM缺少死锁检测并没有必要证明您没有在处理此问题。
犯罪嫌疑人
在此故障排除上下文中,“可疑”定义为具有以下有问题的执行模式的应用程序或中间件代码。
- 使用FLAT锁,然后使用ReentrantLock WRITE锁(执行路径#1)
- 使用ReentrantLock READ锁,然后使用FLAT锁(执行路径#2)
- 由2个Java线程并发执行,但执行顺序相反
上面的锁排序死锁标准可以如下所示:
现在,让我们通过示例Java程序来复制此问题,并查看JVM线程转储输出。
示例Java程序
上面的死锁条件是首先从我们的Oracle OSB问题案例中发现的。 然后,我们通过一个简单的Java程序重新创建了它。 您可以在此处 下载我们程序的完整源代码。 该程序只是创建并触发2个工作线程。 它们每个执行不同的执行路径,并尝试以不同的顺序获取共享对象上的锁。 我们还创建了一个死锁检测器线程以进行监视和记录。 现在,在下面找到实现2条不同执行路径的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线程转储。
从Java线程转储示例中可以看到。 JVM未检测到此死锁条件(例如,不存在“发现一个Java级死锁”),但很明显,这两个线程处于死锁状态。
根本原因:ReetrantLock READ锁定行为
至此,我们发现的主要解释与ReetrantLock READ锁的用法有关。 读锁通常不设计为具有所有权概念。 由于没有哪个线程持有读取锁的记录,因此这似乎可以防止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$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跟踪写入锁。 这意味着HotSpot JVM死锁检测器似乎当前被设计用来检测:
- 对象监视器上涉及FLAT锁的死锁
- 死锁涉及与WRITE锁相关联的已锁定拥有的同步器
缺少读取锁每线程跟踪似乎可以防止这种情况下的死锁检测,并大大增加了故障排除的复杂性。 我建议您阅读Doug Lea在整个问题上的评论 ,因为早在2005年就开始担心由于某些潜在的锁定开销而可能添加每线程读取保持跟踪。 如果您怀疑涉及读取锁的隐藏死锁情况,请在下面的故障排除建议中查找:
- 仔细分析线程调用堆栈跟踪,它可能会揭示某些代码潜在地获取读锁,并阻止其他线程获取写锁。
- 如果您是代码的所有者,请通过使用lock.getReadLockCount()方法来跟踪读取锁的计数。
我期待着您的反馈,特别是对于具有此类涉及读锁的死锁经验的个人。 最后,在下面的视频中找到通过示例Java程序的执行和监视来解释这些发现的视频。
参考: Java并发性: Java EE支持模式和Java教程博客中,我们的JCG合作伙伴 Pierre-Hugues Charbonneau 隐藏的线程死锁 。
翻译自: https://www.javacodegeeks.com/2013/02/java-concurrency-the-hidden-thread-deadlocks.html