首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >线程池报错了,为什么没有异常日志?

线程池报错了,为什么没有异常日志?

作者头像
苏三说技术
发布2026-03-09 15:44:14
发布2026-03-09 15:44:14
770
举报
文章被收录于专栏:苏三说技术苏三说技术

大家好,我是苏三~

昨天下午,运营说有个用户标签更新任务没跑,后台数据全是旧的!

这个任务我前两天才优化过,逻辑很简单,就是从数据库查一批人,算一下标签,再写回去。为了快点,我还特意用了线程池做并发。

我第一时间去翻服务器的日志 error.log。结果让我头皮发麻:日志里干干净净,没有一行 Exception,甚至连个 WARN 都没有。

这就见鬼了。进程活着,线程池没满,监控显示任务也正常触发了。既然任务跑了,如果逻辑出错,肯定会抛异常;如果抛异常,日志肯定会打出来。

但在线程池的世界里,这个常识是错的。

案发现场

为了复现这个问题,我写了一段最简化的代码。大家可以把这段代码复制到 IDEA 里跑一下,亲眼看看什么叫由于沉默而导致的崩溃。

代码语言:javascript
复制
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

publicclass ThreadPoolSwallowException {
    public static void main(String[] args) {
        // 1. 创建一个单线程的线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 2. 提交一个必然报错的任务
        executor.submit(() -> {
            System.out.println("任务开始执行...");
            
            // 这里有个大坑:除以 0,必然抛出 ArithmeticException
            int result = 10 / 0; 
            
            System.out.println("计算结果:" + result);
        });

        // 3. 关闭线程池
        executor.shutdown();
        System.out.println("主线程结束,准备看戏...");
    }
}

运行结果:

代码语言:javascript
复制
主线程结束,准备看戏...
任务开始执行...

看到问题了吗?

控制台打印了任务开始执行...,然后就没了

那个致命的 / by zero 异常,没有堆栈,没有报错,程序就这样优雅地结束了。

如果这是在生产环境,你的业务逻辑就像这段代码一样,断在了中间,而你却一无所知,只能等着用户投诉或者数据错乱。

异常去哪儿了?

为了搞清楚异常到底被谁吞了,我们需要扒开 JDK 的源码看一看。

当你调用 executor.submit() 时,线程池并不是直接跑你的任务,而是把它包装成了一个 FutureTask 对象。

我们来看看 FutureTaskrun() 方法里到底干了什么(JDK 8 源码片段):

代码语言:javascript
复制
public void run() {
    // ... 省略状态检查 ...
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                // 1. 这里真正执行你的业务逻辑
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                // 2. 【重点!】异常被捕获了!
                result = null;
                ran = false;
                
                // 3. 异常被塞到了这里
                setException(ex); 
            }
            if (ran)
                set(result);
        }
    } finally {
        // ...
    }
}

真相大白!

  1. 你的业务代码抛出的异常,在 catch (Throwable ex) 里被捕获了。
  2. 并没有被打印到控制台,也没有被抛给 ThreadUncaughtExceptionHandler
  3. 它被传递给了 setException(ex) 方法。

这个 setException 它把异常对象赋值给了 FutureTask 内部的一个变量 outcome。异常并没有消失,它只是被封存在了这个 Future 对象里。

线程池的逻辑是:我帮你把任务存起来了。想知道结果,想知道有没有报错,那得调用 future.get()来问,你不问我就不说。

如何让异常显形?

既然知道了原因,解决办法就有很多种。这里推荐 3 种最实用的方案。

改用 execute()(最推荐)

如果你提交的任务不需要返回结果,比如定时任务一样,请直接用 execute() 替代 submit()

代码语言:javascript
复制
// 改用 execute
executor.execute(() -> {
    System.out.println("任务开始执行...");
    int result = 10 / 0; 
});

因为 execute() 方法是直接把任务扔给线程跑的,没有 FutureTask 的包装。

一旦抛出异常,线程会直接把异常抛给 JVM 的 UncaughtExceptionHandler,控制台立马就会打印出红色的堆栈信息。

自己 Try-Catch(最稳妥)

不管你用 submit 还是 execute,在最外层包一个 try-catch 永远是保命的最佳实践。

代码语言:javascript
复制
executor.submit(() -> {
    try {
        System.out.println("任务开始执行...");
        int result = 10 / 0; 
    } catch (Exception e) {
        // 自己记录日志,想怎么打就怎么打
        log.error("任务执行发生异常", e);
    }
});

这虽然写起来麻烦点,但能保证异常一定会被你捕获并记录到日志文件中,而不是依赖控制台的标准输出。

调用 Future.get()

如果你非要用 submit 且需要返回值,那你必须在主线程里去获取结果。

代码语言:javascript
复制
Future<?> future = executor.submit(() -> {
    return 10 / 0;
});

try {
    // 这一步会把“封存”的异常重新抛出来
    future.get();
} catch (ExecutionException e) {
    // 真正的异常在 e.getCause() 里
    log.error("任务报错了", e.getCause());
}

但要注意,future.get() 是阻塞的。如果你在主线程直接调用,就失去了异步并发的意义。通常我们会在遍历 Future 列表时才去调用。

总结

线程池吞异常是一个非常不易发现的坑,一般都只有在线上才容易看到,细节决定绩效,记住就行了!

看完等于学会,点个赞吧!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 苏三说技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 案发现场
  • 异常去哪儿了?
  • 如何让异常显形?
    • 改用 execute()(最推荐)
    • 自己 Try-Catch(最稳妥)
    • 调用 Future.get()
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档