每日一博 - 线程池异常处理的三种方案

在这里插入图片描述


一、问题背景

在生产环境中,线程池极大地提升了并发任务处理能力。但如果线程执行过程中抛出异常,往往导致日志难以追踪、任务悄无声息地失败,进而引发系统不稳定甚至数据不一致的问题。因此,掌握在线程池中捕获并处理异常的正确姿势,显得尤为重要。


二、模拟线程池抛异常场景

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 全局未捕获异常处理器,无需修改每个任务:

  1. 自定义 ThreadFactory,在创建线程时绑定 Handler。
  2. 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() 调用- 对所有任务统一处理
- 同时覆盖 executesubmit
- 低侵入
- 需区分 RunnableFuture
- submit 需额外 get()
- 可能阻塞线程
大规模通用线程池,需统一监控- 在自定义线程池类中封装好
- 对 get() 超时与中断做好处理
- 配合监控埋点导出异常指标
  • 上表“性能开销”均指正常运行(无异常)时对吞吐的影响,均非常轻微;在异常路径会有额外少量 CPU 或阻塞开销。
  • 对于对延迟敏感的高并发场景,也可在 afterExecute 中对超时或繁重处理异步化(如日志上报放到另一线程)。
  1. 首选:若任务本身需要自定义补偿或上下文信息,推荐方案一(try–catch)。
  2. 统一处理:推荐方案三,通过 afterExecuteexecutesubmit 统一纳入监控。
  3. 监控告警:关键任务建议结合外部监控系统(如 Prometheus、Sentry 等)进行指标埋点与告警。
  4. 线程工厂:自定义工厂可用于线程隔离、命名规范及全局异常处理。

附 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;
        }
    }
}

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值