线程池的自定义异常处理机制

📅 2026/8/14 17:23:03
线程池的自定义异常处理机制
当线程池里的任务抛出异常时异常信息经常消失了控制台看不到任何报错。这就是线程池默认行为带来的坑。今天我们就来搞清楚怎么接管这些异常。一、问题现象异常被吞掉了先看一段典型代码感受一下问题ExecutorService executor Executors.newFixedThreadPool(2); executor.submit(() - { System.out.println(任务开始执行...); int result 10 / 0; // 这里会抛 ArithmeticException System.out.println(任务执行完毕: result); }); executor.shutdown();运行结果任务开始执行...然后就没了……没有异常堆栈程序也没崩溃仿佛什么都没发生。这就是线程池默认把异常给吞了。原因submit()方法返回一个Future对象异常被封装在Future里。如果你不调用future.get()异常就不会被抛出来。二、自定义异常处理的 4 种方式方式 1在任务内部 try-catch最基础这是最直接的办法把异常处理逻辑写在任务里executor.submit(() - { try { int result 10 / 0; } catch (Exception e) { System.out.println(任务出错了: e.getMessage()); // 这里可以记录日志、发送告警等 } });优点简单直观每个任务自己管自己。缺点每个任务都要写 try-catch代码重复容易漏。方式 2包装一个统一的任务包装器把 try-catch 逻辑抽出来封装成一个工具方法public class TaskWrapper { public static Runnable wrap(Runnable task) { return () - { try { task.run(); } catch (Exception e) { System.out.println(【统一捕获】任务异常: e.getClass().getName() - e.getMessage()); e.printStackTrace(); // 这里可以接入日志框架比如 log.error(...) } }; } } // 使用 executor.submit(TaskWrapper.wrap(() - { int result 10 / 0; }));优点统一处理不用每个任务都写 try-catch。缺点每次提交任务都要包一层稍微麻烦。方式 3自定义 ThreadFactory设置未捕获异常处理器推荐这是线程池级别的统一处理也是最优雅的方式之一。// 1. 自定义 ThreadFactory给每个线程设置 UncaughtExceptionHandler ThreadFactory customThreadFactory r - { Thread t new Thread(r); t.setUncaughtExceptionHandler((thread, throwable) - { System.out.println(【线程池级异常处理】线程 thread.getName() 发生异常: throwable.getMessage()); throwable.printStackTrace(); // 这里可以记录日志、发送钉钉/企业微信告警、统计异常次数等 }); return t; }; // 2. 使用自定义 ThreadFactory 创建线程池 ExecutorService executor new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(100), // 任务队列 customThreadFactory, // 自定义线程工厂 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); // 3. 提交任务用 execute不是 submit executor.execute(() - { System.out.println(任务执行中...); throw new RuntimeException(模拟业务异常); });⚠️关键点UncaughtExceptionHandler只对execute()提交的任务生效。如果用submit()异常会被封装进Future不会触发这个处理器。方式 4自定义 ThreadPoolExecutor重写 afterExecute 方法最强大如果你需要同时处理execute()和submit()的异常可以自定义线程池类重写afterExecute方法public class CustomThreadPool extends ThreadPoolExecutor { public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); // submit() 提交的异常会被包装在 FutureTask 里需要特殊处理 if (t null r instanceof Future?) { try { Future? future (Future?) r; if (future.isDone()) { future.get(); // 这里会抛出异常 } } catch (ExecutionException ee) { t ee.getCause(); // 拿到真正的异常 } catch (InterruptedException | CancellationException ignored) { } } if (t ! null) { System.out.println(【afterExecute 捕获异常】: t.getMessage()); t.printStackTrace(); // 这里可以做日志记录、监控告警、失败重试等 } } } // 使用 CustomThreadPool pool new CustomThreadPool( 2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(100) ); // 用 submit 也能捕获到异常 pool.submit(() - { throw new RuntimeException(submit 抛出的异常); });优点无论execute()还是submit()异常都能统一捕获。缺点稍微复杂一点需要继承ThreadPoolExecutor。三、4 种方式对比总结方式粒度能否捕获 submit 异常复杂度适用场景任务内 try-catch单个任务✅ 能⭐ 简单临时任务、快速修复任务包装器单个任务✅ 能⭐ 简单需要统一包装但不想改线程池自定义 ThreadFactory线程池级别❌ 不能仅 execute⭐⭐ 中等大部分场景配合 execute 使用重写 afterExecute线程池级别✅ 能⭐⭐⭐ 稍复杂需要完整监控、审计、重试机制四、实际工作中的建议简单项目用方式 3自定义 ThreadFactory UncaughtExceptionHandler配合execute()提交任务足够用了。中大型项目用方式 4重写afterExecute接入日志框架SLF4J Logback异常发生时自动记录并推送告警。无论哪种方式不要在异常处理里再抛异常否则可能导致线程池里的线程被销毁影响任务执行。生产环境建议异常处理里做这几件事记录详细日志线程名、任务信息、异常堆栈接入监控告警钉钉、邮件、Prometheus 等考虑失败重试或放入死信队列