文章目录
一、问题背景
在生产环境中,线程池极大地提升了并发任务处理能力。但如果线程执行过程中抛出异常,往往导致日志难以追踪、任务悄无声息地失败,进而引发系统不稳定甚至数据不一致的问题。因此,掌握在线程池中捕获并处理异常的正确姿势,显得尤为重要。
二、模拟线程池抛异常场景
public class ThreadPoolExceptionDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
// submit 无提示,其他线程继续执行
pool.submit(new FaultyTask());
// execute 会打印异常,其他线程继续执行
pool.execute(new FaultyTask());
}
}
class FaultyTask implements Runnable {
@Override
public void run() {
System.out.println("进入任务方法");
int x = 1 / 0; // 触发异常
}
}
-
运行结果:
submit
提交的任务:静默失败,无异常堆栈输出execute
提交的任务:控制台打印java.lang.ArithmeticException: / by zero
三、submit 与 execute 的差异
- execute(Runnable):直接提交,未封装返回值。异常冒泡至最外层由线程池捕获并打印。
- submit(Callable/Runnable):将任务封装为
FutureTask
,内部 catch 掉所有异常,并保存到内部状态,再由调用方通过Future.get()
抛出或获取。
源码简析:
// submit 源码
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask); // 交给 execute 处理
return ftask; // 返回 Future,可通过 get() 获取异常
四、方案一:任务内部 try–catch
为每个任务在 run()
内部添加异常捕获,最直接也最“粗暴”:
class SafeTask implements Runnable {
@Override
public void run() {
try {
System.out.println("进入任务方法");
int x = 1 / 0;
} catch (Exception e) {
System.err.println("捕获到任务异常: " + e);
// 可自定义报警、补偿逻辑等
}
}
}
- 优点:简单易懂,能够针对单个任务定制特殊处理。
- 缺点:每个任务都要重复编写,侵入性高,代码臃肿。
五、方案二:Thread.setDefaultUncaughtExceptionHandler
借助 JVM 全局未捕获异常处理器,无需修改每个任务:
- 自定义
ThreadFactory
,在创建线程时绑定 Handler。 - Handler 在任意线程出现未捕获异常时统一回调。
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) ->
System.err.println("UncaughtExceptionHandler 捕获: " + ex.getMessage()));
return t;
};
ExecutorService pool = new ThreadPoolExecutor(
1,1,0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), factory);
pool.execute(new FaultyTask());
- 注意:
submit
提交的任务异常已被FutureTask
捕获,不会触发此 Handler。适用于execute
场景。
六、方案三:重写 afterExecute 统一处理
通过继承 ThreadPoolExecutor
,重写 afterExecute(Runnable r, Throwable t)
,在任务执行后统一检查并处理异常:
ExecutorService pool = new ThreadPoolExecutor(
2, 3, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) { // execute 方式抛出的异常
System.err.println("afterExecute 捕获 execute 异常: " + t.getMessage());
}
// submit 方式封装为 FutureTask,需要通过 get() 再次抛出
if (r instanceof Future<?>) {
try {
((Future<?>) r).get();
} catch (ExecutionException ee) {
System.err.println("afterExecute 捕获 submit 异常: " + ee.getCause());
} catch (InterruptedException ignored) { }
}
}
};
pool.execute(new FaultyTask());
pool.submit(new FaultyTask());
- 优点:集中处理所有任务异常,侵入性低,适合大规模通用场景。
- 缺点:对
submit
仍需额外判断和get()
,增加一定复杂度。
七、总结
方案 | 实现复杂度 | 性能开销 | 优点 | 缺点 | 适用场景 | 最佳实践 |
---|---|---|---|---|---|---|
方案一:任务内 try–catch | 低 | 极低——只在异常路径有少量捕获开销 | - 最直接、可针对每个任务自定义逻辑 - 异常堆栈立即可见 | - 代码重复、侵入性高 - 难以统一管理 - 大量任务时代码臃肿 | 少量关键任务,需单独补偿或上下文处理 | - 只在真正关键、异步补偿逻辑多的任务中使用 - 保持捕获代码精简 - 异常分级上报到监控系统 |
方案二:全局 UncaughtExceptionHandler | 中 | 低——除异常触发时执行一次 Handler | - 无需修改各任务代码 - 全局统一、命名与隔离清晰 | - 只对 execute 生效- submit 会吞异常不触发- 无任务上下文 | 面向所有不需返回值的异步任务 | - 搭配自定义 ThreadFactory 使用- 配置线程名称与隔离组 - 异常时上报集中告警或日志系统 |
方案三:重写 afterExecute | 中 | 低——每次任务完成后多一次判断与 Future.get() 调用 | - 对所有任务统一处理 - 同时覆盖 execute 与 submit - 低侵入 | - 需区分 Runnable 与 Future - submit 需额外 get() - 可能阻塞线程 | 大规模通用线程池,需统一监控 | - 在自定义线程池类中封装好 - 对 get() 超时与中断做好处理- 配合监控埋点导出异常指标 |
- 上表“性能开销”均指正常运行(无异常)时对吞吐的影响,均非常轻微;在异常路径会有额外少量 CPU 或阻塞开销。
- 对于对延迟敏感的高并发场景,也可在
afterExecute
中对超时或繁重处理异步化(如日志上报放到另一线程)。
- 首选:若任务本身需要自定义补偿或上下文信息,推荐方案一(try–catch)。
- 统一处理:推荐方案三,通过
afterExecute
将execute
与submit
统一纳入监控。 - 监控告警:关键任务建议结合外部监控系统(如 Prometheus、Sentry 等)进行指标埋点与告警。
- 线程工厂:自定义工厂可用于线程隔离、命名规范及全局异常处理。
附 Code
import java.util.concurrent.*;
/**
* 完整演示:线程池异常处理 Demo
* 方案:自定义 ThreadPoolExecutor,重写 afterExecute 方法,统一捕获 execute 与 submit 提交的异常
*/
public class ThreadPoolExceptionDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 创建自定义线程池
ExecutorService pool = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new NamedThreadFactory("demo-pool")) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// 处理 execute 提交时的异常
if (t != null) {
System.err.println("[afterExecute] execute 异常: " + t);
}
// 处理 submit 提交时的异常(FutureTask 会吞掉异常)
if (r instanceof Future<?>) {
try {
((Future<?>) r).get();
} catch (CancellationException ce) {
System.err.println("[afterExecute] 任务被取消: " + ce);
} catch (ExecutionException ee) {
System.err.println("[afterExecute] submit 异常: " + ee.getCause());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
};
// 2. 使用 execute 提交带异常任务
System.out.println("==> 使用 execute 提交任务");
pool.execute(new FaultyTask("ExecuteTask"));
// 3. 使用 submit 提交带异常任务
System.out.println("==> 使用 submit 提交任务");
pool.submit(new FaultyTask("SubmitTask"));
// 4. 关闭线程池前等待一段时间,确保任务完成
pool.shutdown();
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
System.out.println("Demo 完成");
}
/**
* 带异常的任务,运行时会触发除零异常
*/
static class FaultyTask implements Runnable {
private final String name;
FaultyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("[Task " + name + "] 开始执行");
int x = 1 / 0; // 触发异常
}
}
/**
* 自定义线程工厂:为每个线程命名,并可设置全局 UncaughtExceptionHandler
*/
static class NamedThreadFactory implements ThreadFactory {
private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
private final String prefix;
private int counter = 0;
NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = defaultFactory.newThread(r);
t.setName(prefix + ".thread-" + (++counter));
t.setUncaughtExceptionHandler((thread, ex) ->
System.err.println("[UncaughtExceptionHandler] [" + thread.getName() + "] 异常: " + ex));
return t;
}
}
}