org.h2.util.AbbaLockingDetector Maven / Gradle / Ivy
/*
* Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.util;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Utility to detect AB-BA deadlocks.
*/
public class AbbaLockingDetector implements Runnable {
private final int tickIntervalMs = 2;
private volatile boolean stop;
private final ThreadMXBean threadMXBean =
ManagementFactory.getThreadMXBean();
private Thread thread;
/**
* Map of (object A) -> ( map of (object locked before object A) ->
* (stack trace where locked) )
*/
private final Map> lockOrdering =
new WeakHashMap<>();
private final Set knownDeadlocks = new HashSet<>();
/**
* Start collecting locking data.
*
* @return this
*/
public AbbaLockingDetector startCollecting() {
thread = new Thread(this, "AbbaLockingDetector");
thread.setDaemon(true);
thread.start();
return this;
}
/**
* Reset the state.
*/
public synchronized void reset() {
lockOrdering.clear();
knownDeadlocks.clear();
}
/**
* Stop collecting.
*
* @return this
*/
public AbbaLockingDetector stopCollecting() {
stop = true;
if (thread != null) {
try {
thread.join();
} catch (InterruptedException e) {
// ignore
}
thread = null;
}
return this;
}
@Override
public void run() {
while (!stop) {
try {
tick();
} catch (Throwable t) {
break;
}
}
}
private void tick() {
if (tickIntervalMs > 0) {
try {
Thread.sleep(tickIntervalMs);
} catch (InterruptedException ex) {
// ignore
}
}
ThreadInfo[] list = threadMXBean.dumpAllThreads(
// lockedMonitors
true,
// lockedSynchronizers
false);
processThreadList(list);
}
private void processThreadList(ThreadInfo[] threadInfoList) {
final List lockOrder = new ArrayList<>();
for (ThreadInfo threadInfo : threadInfoList) {
lockOrder.clear();
generateOrdering(lockOrder, threadInfo);
if (lockOrder.size() > 1) {
markHigher(lockOrder, threadInfo);
}
}
}
/**
* We cannot simply call getLockedMonitors because it is not guaranteed to
* return the locks in the correct order.
*/
private static void generateOrdering(final List lockOrder,
ThreadInfo info) {
final MonitorInfo[] lockedMonitors = info.getLockedMonitors();
Arrays.sort(lockedMonitors, new Comparator() {
@Override
public int compare(MonitorInfo a, MonitorInfo b) {
return b.getLockedStackDepth() - a.getLockedStackDepth();
}
});
for (MonitorInfo mi : lockedMonitors) {
String lockName = getObjectName(mi);
if (lockName.equals("sun.misc.Launcher$AppClassLoader")) {
// ignore, it shows up everywhere
continue;
}
// Ignore locks which are locked multiple times in
// succession - Java locks are recursive.
if (!lockOrder.contains(lockName)) {
lockOrder.add(lockName);
}
}
}
private synchronized void markHigher(List lockOrder,
ThreadInfo threadInfo) {
String topLock = lockOrder.get(lockOrder.size() - 1);
Map map = lockOrdering.get(topLock);
if (map == null) {
map = new WeakHashMap<>();
lockOrdering.put(topLock, map);
}
String oldException = null;
for (int i = 0; i < lockOrder.size() - 1; i++) {
String olderLock = lockOrder.get(i);
Map oldMap = lockOrdering.get(olderLock);
boolean foundDeadLock = false;
if (oldMap != null) {
String e = oldMap.get(topLock);
if (e != null) {
foundDeadLock = true;
String deadlockType = topLock + " " + olderLock;
if (!knownDeadlocks.contains(deadlockType)) {
System.out.println(topLock + " synchronized after \n " + olderLock
+ ", but in the past before\n" + "AFTER\n" +
getStackTraceForThread(threadInfo)
+ "BEFORE\n" + e);
knownDeadlocks.add(deadlockType);
}
}
}
if (!foundDeadLock && !map.containsKey(olderLock)) {
if (oldException == null) {
oldException = getStackTraceForThread(threadInfo);
}
map.put(olderLock, oldException);
}
}
}
/**
* Dump data in the same format as {@link ThreadInfo#toString()}, but with
* some modifications (no stack frame limit, and removal of uninteresting
* stack frames)
*/
private static String getStackTraceForThread(ThreadInfo info) {
StringBuilder sb = new StringBuilder().append('"')
.append(info.getThreadName()).append("\"" + " Id=")
.append(info.getThreadId()).append(' ').append(info.getThreadState());
if (info.getLockName() != null) {
sb.append(" on ").append(info.getLockName());
}
if (info.getLockOwnerName() != null) {
sb.append(" owned by \"").append(info.getLockOwnerName())
.append("\" Id=").append(info.getLockOwnerId());
}
if (info.isSuspended()) {
sb.append(" (suspended)");
}
if (info.isInNative()) {
sb.append(" (in native)");
}
sb.append('\n');
final StackTraceElement[] stackTrace = info.getStackTrace();
final MonitorInfo[] lockedMonitors = info.getLockedMonitors();
boolean startDumping = false;
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement e = stackTrace[i];
if (startDumping) {
dumpStackTraceElement(info, sb, i, e);
}
for (MonitorInfo mi : lockedMonitors) {
if (mi.getLockedStackDepth() == i) {
// Only start dumping the stack from the first time we lock
// something.
// Removes a lot of unnecessary noise from the output.
if (!startDumping) {
dumpStackTraceElement(info, sb, i, e);
startDumping = true;
}
sb.append("\t- locked ").append(mi);
sb.append('\n');
}
}
}
return sb.toString();
}
private static void dumpStackTraceElement(ThreadInfo info,
StringBuilder sb, int i, StackTraceElement e) {
sb.append('\t').append("at ").append(e)
.append('\n');
if (i == 0 && info.getLockInfo() != null) {
Thread.State ts = info.getThreadState();
switch (ts) {
case BLOCKED:
sb.append("\t- blocked on ")
.append(info.getLockInfo())
.append('\n');
break;
case WAITING:
sb.append("\t- waiting on ")
.append(info.getLockInfo())
.append('\n');
break;
case TIMED_WAITING:
sb.append("\t- waiting on ")
.append(info.getLockInfo())
.append('\n');
break;
default:
}
}
}
private static String getObjectName(MonitorInfo info) {
return info.getClassName() + "@" +
Integer.toHexString(info.getIdentityHashCode());
}
}