利用ThreadMXBean实现检测死锁

82 阅读4分钟

在使用JConsole的时候,在线程页下,可以看到一个检测死锁按钮,很好奇它是如何获取死锁线程的。同时检测死锁算法也是操作系统的重要算法之一,本文在参考了JConsole源码的基础上来实现检测死锁的功能。 获取源码 github.com/maoturing/b…

1.检测死锁类的属性和方法

public class DeadlockDetector{

  //获取ThreadMXBean 
  private final ThreadMXBean mBean = ManagementFactory.getThreadMXBean();

  //处理检测到的死锁线程
  public void handleDeadLock(ThreadInfo[] deadLockThreads)//得到线程锁定的对象
  public void getThreadLock(long selected)//得到线程和线程所在的组,K为线程Id,V为所在的组,组以死锁划分,一个死锁为一组
  public Map getDeadlockedGroup() throws IOException;

2.获取死锁线程

// 查找因为等待获得对象监视器或可拥有同步器而处于死锁状态的线程循环。返回线程ID
long[] ids = mBean.findDeadlockedThreads(); 

// 根据死锁线程id得到死锁线程信息,参数2代表的是获取同步监视器,后面获取线程锁定对象时使用
ThreadInfo[] threadInfos = mBean.getThreadInfo(ids , true, false); 

3.处理死锁线程

public void handleDeadLock(ThreadInfo[] deadLockThreads) {
		if (deadLockThreads != null) {
			System.err.println("Deadlock detected!");

			for (ThreadInfo threadInfo : deadLockThreads) {
				if (threadInfo != null) {
					// SecurityException
					for (Thread thread : Thread.getAllStackTraces().keySet()) {
						if (thread.getId() == threadInfo.getThreadId()) {
							System.out.println("id:" + threadInfo.getThreadId());
							System.out.println("名称:" + threadInfo.getThreadName());
							System.out.print("状态:" + threadInfo.getLockName());
							System.out.println("上的" + threadInfo.getThreadState());
							System.out.println("拥有者:" + threadInfo.getLockOwnerName());
							System.out.println("总阻止数:" + threadInfo.getBlockedCount());
							System.out.println("总等待数:" + threadInfo.getWaitedCount());
							System.out.println("状态:" + threadInfo.toString());

							String name = mBean.getThreadInfo(threadInfo.getLockOwnerId()).getLockName();
							System.out.println("已锁定:" + name);
							int i = 0;

							MonitorInfo[] monitors = threadInfo.getLockedMonitors();

							for (StackTraceElement ste : thread.getStackTrace()) {
								System.err.println("堆栈深度:"+thread.getStackTrace().length);
								System.err.println("堆栈信息:"+ste.toString());

								System.out.println("拼接的堆栈信息:"+ste.getClassName() + ste.getMethodName() + ste.getFileName()
										+ ste.getLineNumber());
								selectThread(threadInfo.getThreadId());
                                			}
						}

					}
					System.out.println("==================");
				}
			}

		} else {
			System.out.println("未检测到死锁线程");
		}

	}

4.得到线程锁定的对象 首先需要得到线程对象,mBean.getThreadInfo(deadlockedThreads, true, false)方法的第二个参数为true表示获得同步监视器,

public void getThreadLock(long selected) {
		final long threadID = selected;
		StringBuilder sb = new StringBuilder();
		ThreadInfo ti = null;
		MonitorInfo[] monitors = null;
			for (ThreadInfo info : infos) {
				if (info.getThreadId() == threadID) {
					ti = info;
					monitors = info.getLockedMonitors();
					break;
				}
			}
			System.out.println("support");
		if (ti != null) {
			int index = 0;
			if (monitors != null) {
				for (MonitorInfo mi : monitors) {
					if (mi.getLockedStackDepth() == index) {
						System.out.println("已锁定:" + mi.toString());
					}
				}
				index++;
			}
		}

	}

5.得到线程所在的组(检测死锁算法) 线程所在的组的区分标准是线程是否属于同一个死锁,所谓死锁是指两个或两个以上的线程,互相竞争资源导致的一种阻塞现象,最简单的例子就是线程 T1 已锁定B对象,然后去请求A对象,而线程 T2 已锁定A对象,然后去请求B对象,导致二者一直阻塞下去。 两个线程竞争资源导致的死锁.png

当然,实际可能出现的情况会更复杂,我们如何来判断哪些线程属于同一个死锁呢? 在下图中可以看出,无论是哪种死锁形式,都会形成一个闭环。根据这个特点,我们可以将死锁抽象为有向图,多个死锁就是多个有向图,我们改造一下有向图的遍历算法即可得到所有的死锁以及死锁拥有的线程。 三种死锁形式 首先遍历所有被阻塞的线程T1~T7,第一次先得到线程T1,由于T1被B对象阻塞,我们使用getLockOwnerId()方法获得B对象的拥有者线程T2,并将T1的访问标志visited[j]置为true,继续执行.......,直到得到了线程T3,用getLockOwnerId()方法获得线程T1,此时发现线程T1的访问标志visited[j]已经为true,意味着我们已经完成了一个死锁的遍历,之前遍历过的线程组成了1个死锁,此时标志死锁个数的gid++,继续遍历下一个死锁。

	public Map getDeadlockedGroup() throws IOException {

		long[] ids = mBean.findDeadlockedThreads();
		if (ids == null) {
			return null;
		}
		ThreadInfo[] infos = mBean.getThreadInfo(ids, Integer.MAX_VALUE);

		List<Long[]> dcycles = new ArrayList<Long[]>();
		List<Long> cycle = new ArrayList<Long>();
		Map<Long, Integer> map = new HashMap<Long, Integer>();
		int gid = 1;
		boolean[] visited = new boolean[ids.length];

		int index = -1; // Index into arrays
		while (true) {
			if (index < 0) {
				if (map.size() > 0) {
					// a cycle found
					// dcycles.add(cycle.toArray(new Long[0]));
					gid++;
					// cycle = new ArrayList<Long>();
				}
				// start a new cycle from a non-visited thread
				for (int j = 0; j < ids.length; j++) {
					if (!visited[j]) {
						index = j;
						visited[j] = true;
						break;
					}
				}

				// 当所有线程均被访问过,退出while循环
				if (index < 0) {
					// done
					break;
				}
			}

			// cycle.add(ids[index]);
			map.put(ids[index], gid);
			long nextThreadId = infos[index].getLockOwnerId();

			for (int j = 0; j < ids.length; j++) {
				ThreadInfo ti = infos[j];
				if (ti.getThreadId() == nextThreadId) {
					if (visited[j]) {
						index = -1;
					} else {
						index = j;
						visited[j] = true;
					}
					break;
				}
			}
		}
        //线程id为key,所在死锁组为value
		return map;
	}

对于死锁分组,由于遍历算法的原因,可能会出现一个线程组成的死锁的小bug,比如“图-三种死锁形式”中的第三种情况,如果从T8开始遍历,那么T8和T7组成一个死锁,T6单独组成一个死锁,这种情况极为少见,JConsole也是这样处理,所以这里就不深入追究了。以上,就是java利用ThreadMXBean获取线程死锁的所有内容。