最近在看 ​​dubbo​​​ 源码,真的学习了很多知识,记得上次看 ​​dubbo​​​ 源码是在半年前了,看到 ​​dubbo​​​ 自定义的 ​​spi​​​ 就看不下去了,完全看不懂。上周又看了看,顿时茅塞顿开,有时间会分享出来。废话不多说,在 ​​dubbo​​​ 内部有几种线程模型,都是使用 ​​java​​ 线程池实现的,任务被拒绝后会输出堆栈信息。我们可以看它是怎么实现的。

package org.apache.dubbo.common.threadpool.support;

import org.apache.dubbo.common.Constants;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.JVMUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;

/**
* Abort Policy.
* Log warn info when abort.
*/
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);

private final String threadName;

private final URL url;

private static volatile long lastPrintTime = 0;

private static Semaphore guard = new Semaphore(1);

public AbortPolicyWithReport(String threadName, URL url) {
this.threadName = threadName;
this.url = url;
}

@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}

private void dumpJStack() {
long now = System.currentTimeMillis();

//dump every 10 minutes
if (now - lastPrintTime < 10 * 60 * 1000) {
return;
}

if (!guard.tryAcquire()) {
return;
}

//有内存泄漏的危险
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
String dumpPath = url.getParameter(Constants.DUMP_DIRECTORY, System.getProperty("user.home"));

SimpleDateFormat sdf;

String OS = System.getProperty("os.name").toLowerCase();

// window system don't support ":" in file name
if(OS.contains("win")){
sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
}else {
sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
}

String dateStr = sdf.format(new Date());
FileOutputStream jstackStream = null;
try {
jstackStream = new FileOutputStream(new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr));
JVMUtil.jstack(jstackStream);
} catch (Throwable t) {
logger.error("dump jstack error", t);
} finally {
guard.release();
if (jstackStream != null) {
try {
jstackStream.flush();
jstackStream.close();
} catch (IOException e) {
}
}
}

lastPrintTime = System.currentTimeMillis();
}
});

}

}

通过源码可以看到 ​​dubbo​​​ 继承了 ​​ThreadPoolExecutor.AbortPolicy​​​ ,重写了 ​​rejectedExecution​​​ 方法,并且 ​​dubbo​​​ 线程模型的所有拒绝策略都是使用的​​AbortPolicyWithReport​​​ 。在输出线程池信息的同时调用了 ​​dumpJStack​​​ 方法。而在 ​​dumpJStack​​​ 方法里面,新开了一个线程,然后新建了一个文件输出流使用 ​​JVMUtil​​​ 工具类 ​​dump​​​ 堆栈信息。
在这里不得不说一下,在线程池不使用的使用一定要关闭,不然就会造成内存泄漏的危险。在这里 ​​​dubbo​​​ 并没有关闭线程池,提交了一个 ​​pr#3255​​​,看看会不会被 ​​merge​​ 。

继续往下看 ​​JVMUtil​​ 类。

public static void jstack(OutputStream stream) throws Exception {
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
for (ThreadInfo threadInfo : threadMxBean.dumpAllThreads(true, true)) {
stream.write(getThreadDumpString(threadInfo).getBytes());
}
}

可以看到 ​​jstack​​​ 方法使用了 ​​ManagementFactory​​​ 类的 ​​getThreadMXBean​​​ 方法,该方法
能够获得 ​​​Java​​​ 虚拟机线程系统的管理接口。然后通过该接口 ​​dump​​ 所有的线程信息。

dumpAllThreads(boolean lockedMonitors, boolean lockedSynchronizers) 

有两个参数,第一个参数是 ​​monitor​​​锁即同步关键字 ​​synchronized​​​ ,第二个关键字常指的 ​​AQS​​ 锁。

然后通过遍历线程来处理线程信息。

private static String getThreadDumpInfo(ThreadInfo threadInfo) {

StringBuilder sb = new StringBuilder("\"" + threadInfo.getThreadName() + "\"" +
" Id=" + threadInfo.getThreadId() + " " +
threadInfo.getThreadState());
if (threadInfo.getLockName() != null) {
sb.append(" on ").append(threadInfo.getLockName());
}
if (threadInfo.getLockOwnerName() != null) {
sb.append(" owned by \"").append(threadInfo.getLockOwnerName()).append("\" Id=").append(threadInfo.getLockOwnerId());
}
if (threadInfo.isSuspended()) {
sb.append(" (suspended)");
}
if (threadInfo.isInNative()) {
sb.append(" (in native)");
}
sb.append('\n');
int i = 0;

StackTraceElement[] stackTrace = threadInfo.getStackTrace();
MonitorInfo[] lockedMonitors = threadInfo.getLockedMonitors();
for (; i < stackTrace.length && i < 32; i++) {
StackTraceElement ste = stackTrace[i];
sb.append("\tat ").append(ste.toString());
sb.append('\n');
if (i == 0 && threadInfo.getLockInfo() != null) {
Thread.State ts = threadInfo.getThreadState();
switch (ts) {
case BLOCKED:
sb.append("\t- blocked on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
case WAITING:
sb.append("\t- waiting on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
case TIMED_WAITING:
sb.append("\t- waiting on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
default:
}
}

for (MonitorInfo mi : lockedMonitors) {
if (mi.getLockedStackDepth() == i) {
sb.append("\t- locked ").append(mi);
sb.append('\n');
}
}
}
if (i < stackTrace.length) {
sb.append("\t...");
sb.append('\n');
}

LockInfo[] locks = threadInfo.getLockedSynchronizers();
if (locks.length > 0) {
sb.append("\n\tNumber of locked synchronizers = ").append(locks.length);
sb.append('\n');
for (LockInfo li : locks) {
sb.append("\t- ").append(li);
sb.append('\n');
}
}
sb.append('\n');
return sb.toString();
}

里面主要就是对 ​​ThreadInfo​​​ 信息的处理以及文本的格式处理,感兴趣的可以看看。
通过这种方式,我们就可以在前端界面设置一个按钮,触发堆栈信息来查看程序的执行状态,再也不用到机器上使用 ​​​jstack​​​ 命令了
最后直接调用

JVMUtil.jstack

尝试运行一下:

"Signal Dispatcher" Id=4 RUNNABLE


"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@7ea987ac
at java.lang.Object.wait(Native Method)
- waiting on java.lang.ref.ReferenceQueue$Lock@7ea987ac
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)


"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@12a3a380
at java.lang.Object.wait(Native Method)
- waiting on java.lang.ref.Reference$Lock@12a3a380
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)


"main" Id=1 RUNNABLE
at sun.management.ThreadImpl.dumpThreads0(Native Method)
at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:454)
at com.dfire.common.util.DumpStacks.main(DumpStacks.java:14)

参考:​​dubbo​