线程池是作为池化技术的一种常见应用,今天来聊一聊线程池的异常日志处理策略!
先看一个栗子,先定义一个线程池实例
@Bean
public ThreadPoolExecutor uncaughtExceptionExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().
setNameFormat("uncaughtException-worker-%d").build();
int processors = Runtime.getRuntime().availableProcessors();
log.info("processors:{}", processors);
ThreadPoolExecutor executor = new ThreadPoolExecutor(processors,
processors * 2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
return executor;
}
测试案例
@Override
public void executorLogTest() throws Exception {
int count = 10;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
uncaughtExceptionExecutor.execute(() -> {
String str = null;
log.info("execute str:{}", str);
int length = str.length();
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("concurrent execute finished!");
}
意图很明显,第十行会报空指针,希望会在日志文件中将堆栈信息的日志打印出来,但真实的情况会是什么样的呢?
2022-04-30 11:54:26.593 [uncaughtException-worker-5] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.593 [uncaughtException-worker-3] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.593 [uncaughtException-worker-2] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.593 [uncaughtException-worker-0] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.593 [uncaughtException-worker-4] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.593 [uncaughtException-worker-1] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.599 [uncaughtException-worker-10] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.598 [uncaughtException-worker-8] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.599 [uncaughtException-worker-6] INFO c.t.m.s.i.UserServiceImpl - execute str:null
2022-04-30 11:54:26.599 [uncaughtException-worker-9] INFO c.t.m.s.i.UserServiceImpl - execute str:null
怎么log文件中没有异常的堆栈日志呢?通过分析源码ThreadPoolExecutor 会发现这个问题的根源!
接着看afterExecute()方法
此时答案已经浮出水面了,线程池为了不影响其他线程的执行,会将当前线程的异常全部捕获,但是这样会带来一个问题,若开发人员忘记了处理异常,就会莫名缺少一些关键的堆栈日志,处理这个问题主要有三种方案。
最简单直接的方法就是自己动手丰衣足食,将线程池中的业务逻辑catch起来然后自己处理,这个方案会带来代码上的冗余,尤其是业务代码没有checkException,另外有时开发人员也会忘记添加catch,显然,此方案不是最优方案;
@Override
public void executorLogTest() throws Exception {
int count = 10;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
uncaughtExceptionExecutor.execute(() -> {
try {
String str = null;
log.info("execute str:{}", str);
int length = str.length();
} catch (Exception e) {
log.error("executorLogTest occur error", e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
log.info("concurrent execute finished!");
}
按照这种方式处理后,log文件中会显示堆栈信息
package com.tml.mouseDemo.model;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
log.error("CustomThreadPoolExecutor occur error", t);
}
}
测试用例
@Override
public void customExecutor() throws InterruptedException {
int count = 10;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
customExecutor.execute(() -> {
String str = null;
log.info("customExecutor str:{}", str);
int length = str.length();
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("concurrent execute finished!");
}
执行日志结果
Thread.UncaughtExceptionHandler exceptionHandler = (Thread t, Throwable e) -> {
log.info("current thread occurs error!", e);
};
@Bean
public ThreadPoolExecutor executor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().
setUncaughtExceptionHandler(exceptionHandler).
setNameFormat("mouse-worker-%d").build();
int processors = Runtime.getRuntime().availableProcessors();
log.info("processors:{}", processors);
ThreadPoolExecutor executor = new ThreadPoolExecutor(processors,
processors * 2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
测试用例
@Override
public void executorLogTest() throws Exception {
int count = 10;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
executor.execute(() -> {
String str = null;
log.info("execute str:{}", str);
int length = str.length();
});
}
countDownLatch.await();
log.info("concurrent execute finished!");
}
执行结果
实际应用中,第二种和第三种方案,可以根据自己的喜好自行选择。但若你在程序中使用了CountDownLatch,那么官方的建议就是需要try-catch-finally三连的。