在使用JConsole的時候,在線程頁下,可以看到一個檢測死鎖按鈕,很好奇它是如何獲取死鎖線程的。同時檢測死鎖算法也是操作系統(tǒng)的重要算法之一,本文在參考了JConsole源碼的基礎上來實現(xiàn)檢測死鎖的功能。
獲取源碼
https://github.com/maoturing/bescode/tree/master/src/com/requirement/detectdeadlock
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.獲取死鎖線程
// 查找因為等待獲得對象監(jiān)視器或可擁有同步器而處于死鎖狀態(tài)的線程循環(huán)。返回線程ID
long[] ids = mBean.findDeadlockedThreads();
// 根據(jù)死鎖線程id得到死鎖線程信息,參數(shù)2代表的是獲取同步監(jiān)視器,后面獲取線程鎖定對象時使用
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("狀態(tài):" + threadInfo.getLockName());
System.out.println("上的" + threadInfo.getThreadState());
System.out.println("擁有者:" + threadInfo.getLockOwnerName());
System.out.println("總阻止數(shù):" + threadInfo.getBlockedCount());
System.out.println("總等待數(shù):" + threadInfo.getWaitedCount());
System.out.println("狀態(tài):" + 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)方法的第二個參數(shù)為true表示獲得同步監(jiān)視器,
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.得到線程所在的組(檢測死鎖算法)
線程所在的組的區(qū)分標準是線程是否屬于同一個死鎖,所謂死鎖是指兩個或兩個以上的線程,互相競爭資源導致的一種阻塞現(xiàn)象,最簡單的例子就是線程 T1 已鎖定B對象,然后去請求A對象,而線程 T2 已鎖定A對象,然后去請求B對象,導致二者一直阻塞下去。

當然,實際可能出現(xiàn)的情況會更復雜,我們?nèi)绾蝸砼袛嗄男┚€程屬于同一個死鎖呢?
在下圖中可以看出,無論是哪種死鎖形式,都會形成一個閉環(huán)。根據(jù)這個特點,我們可以將死鎖抽象為有向圖,多個死鎖就是多個有向圖,我們改造一下有向圖的遍歷算法即可得到所有的死鎖以及死鎖擁有的線程。

首先遍歷所有被阻塞的線程T1~T7,第一次先得到線程T1,由于T1被B對象阻塞,我們使用getLockOwnerId()方法獲得B對象的擁有者線程T2,并將T1的訪問標志visited[j]置為true,繼續(xù)執(zhí)行.......,直到得到了線程T3,用getLockOwnerId()方法獲得線程T1,此時發(fā)現(xiàn)線程T1的訪問標志visited[j]已經(jīng)為true,意味著我們已經(jīng)完成了一個死鎖的遍歷,之前遍歷過的線程組成了1個死鎖,此時標志死鎖個數(shù)的gid++,繼續(xù)遍歷下一個死鎖。
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循環(huán)
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;
}
對于死鎖分組,由于遍歷算法的原因,可能會出現(xiàn)一個線程組成的死鎖的小bug,比如“圖-三種死鎖形式”中的第三種情況,如果從T8開始遍歷,那么T8和T7組成一個死鎖,T6單獨組成一個死鎖,這種情況極為少見,JConsole也是這樣處理,所以這里就不深入追究了。以上,就是java利用ThreadMXBean獲取線程死鎖的所有內(nèi)容。