背景
某业务场景由异步 + 消息链路保证一定执行成功。
预发消息链路执行次数增加,可以推测出异步链路执行肯定失败了,但是经过排查,并没有看到相关的报错日志
。只能直接看部署分支的代码,排查下来发现异步代码中存在NPE隐患
。修复隐患后消息链路执行次数恢复正常,问题暂时解决,但为什么报错了没有日志?
UncaughtExceptionHandler
直接上结论,由于异步线程池没有配置 UncaughtExceptionHandler
,当异步线程执行过程中发生异常时将不会被捕获,造成异常逃逸现象。
/**
* Set the handler invoked when this thread abruptly terminates
* due to an uncaught exception.
* <p>A thread can take full control of how it responds to uncaught
* exceptions by having its uncaught exception handler explicitly set.
* If no such handler is set then the thread's <tt>ThreadGroup</tt>
* object acts as its handler.
* @param eh the object to use as this thread's uncaught exception
* handler. If <tt>null</tt> then this thread has no explicit handler.
* @throws SecurityException if the current thread is not allowed to
* modify this thread.
* @see #setDefaultUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
异步线程池初始化
在初始化异步线程池的时候指定 UncaughtExceptionHandler,做好异常处理和日志埋点。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class CustomThreadPoolUtil {
/**
* 默认存活时间
*/
private static final long DEFAULT_KEEP_ALIVE_TIME = 5000L;
/**
* 创建线程池,捕获了异步线程中发生的异常,使用方可配置monitor告警及时发现异常
*
* @param coreSize 核心线程数
* @param queueSize 阻塞队列大小
* @param threadName 线程池名
* @param handler 拒绝策略
* @return ThreadPoolExecutor
*/
public static ThreadPoolExecutor newFixedThreadPoolWithThreadName(int coreSize, int queueSize, String threadName, RejectedExecutionHandler handler) {
ThreadFactory threadFactory = createThreadFactor(threadName);
ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(queueSize, true);
if (handler != null) {
return new ThreadPoolExecutor(coreSize, coreSize, DEFAULT_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, blockingQueue, threadFactory, handler);
} else {
return new ThreadPoolExecutor(coreSize, coreSize, DEFAULT_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, blockingQueue, threadFactory);
}
}
public static ThreadFactory createThreadFactor(String threadName) {
if (threadName == null || threadName.length() == 0) {
throw new IllegalArgumentException("thread name is blank");
}
return new ThreadFactory() {
// 线程计数器
private final AtomicLong threadNumber = new AtomicLong(0);
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, threadName + "_" + threadNumber.getAndIncrement());
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
thread.setUncaughtExceptionHandler((t, e) -> {
// TODO log and alarm
System.out.println("threadName:" + Thread.currentThread().getName() + " This is an alarm!!! custom thread pool execute exception, exp:" + e.getMessage());
});
return thread;
}
};
}
}
使用对比
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolTest {
private static final ThreadPoolExecutor DEFAULT_THREAD_POOL = new ThreadPoolExecutor(
4,
4,
1,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100, true),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
private static final ThreadPoolExecutor CUSTOM_THREAD_POOL = CustomThreadPoolUtil.newFixedThreadPoolWithThreadName(
4,
100,
"senbo_thread",
null);
public static void main(String[] args) {
asyncExecuteByDefaultThreadPool();
asyncExecuteByCustomThreadPool();
System.out.println("main finished.");
}
private static void asyncExecuteByDefaultThreadPool() {
try {
DEFAULT_THREAD_POOL.execute(() -> {
// 未设置 UncaughtExceptionHandler,将打印ThreadGroup
System.out.println("threadName:" + Thread.currentThread().getName() + " UncaughtExceptionHandler:" + Thread.currentThread().getUncaughtExceptionHandler().getClass().getName());;
String str = "1x";
System.out.println(Integer.valueOf(str));
System.out.println("default thread pool execute success");
});
} catch (Exception e) {
System.out.println("default thread pool execute exception:" + e.getMessage());
}
}
private static void asyncExecuteByCustomThreadPool() {
try {
CUSTOM_THREAD_POOL.execute(() -> {
System.out.println("threadName:" + Thread.currentThread().getName() + " UncaughtExceptionHandler:" + Thread.currentThread().getUncaughtExceptionHandler().getClass().getName());;
String str = "1x";
System.out.println(Integer.valueOf(str));
System.out.println("custom thread pool execute success");
});
} catch (Exception e) {
System.out.println("custom thread pool execute exception:" + e.getMessage() + ", "+ e.getLocalizedMessage());
}
}
}
打印日志如下,可以看到 default异步线程池没有捕获到线程内部发生的异常,而 custom异步线程池捕获到了异常,并打印了相印的告警日志。
threadName:pool-1-thread-1 UncaughtExceptionHandler:java.lang.ThreadGroup
main finished.
threadName:senbo_thread_0 UncaughtExceptionHandler:thread.CustomThreadPoolUtil$1$$Lambda$3/1831932724
threadName:senbo_thread_0 This is an alarm!!! custom thread pool execute exception, exp:For input string: "1x"
Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: For input string: "1x"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.valueOf(Integer.java:766)
at thread.CustomThreadPoolTest.lambda$asyncExecuteByDefaultThreadPool$0(CustomThreadPoolTest.java:43)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)