当前位置: 首页 > news >正文

线程池里的代码明明报错了,为什么控制台一行异常日志都不打?

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

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

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

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

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

案发现场

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

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("主线程结束,准备看戏..."); } }

运行结果:

主线程结束,准备看戏... 任务开始执行...

看到问题了吗?

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

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

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

异常去哪儿了?

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

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

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

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()

// 改用 execute executor.execute(() -> { System.out.println("任务开始执行..."); int result = 10 / 0; });

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

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

自己 Try-Catch(最稳妥)

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

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

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

调用Future.get()

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

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

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

总结

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

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

http://www.jsqmd.com/news/482316/

相关文章:

  • 《Mastering Atari with Discrete World Models》随记
  • 11 张图总结下,微服务增量拉取
  • STM32入门(10)
  • 打开网站显示图片上传失败?错误怎么办|已解决
  • 校园网线是否可以通过两个路由器进行中转?
  • PHP 网站完整搬家避坑指南(新手必看,杜绝报错、断站)
  • Java 后端实现 token自动续期,这方案有点优雅!
  • AI 批量图片去水印工具 v1.0.0 - 豆包专属去水印
  • 分发:AI的终极护城河
  • LLM可观测性:AI系统缺失的环节
  • 面试官问:订单30分钟未支付,自动取消,该怎么实现?
  • 香河婚介所里的无数次擦肩,终在免费缘分中寻得 IT 人的安稳归宿
  • MySQL 1045 登录失败,账号密码错误处理 常见错误与避坑指南
  • 应该使用AI构建内部工具吗?
  • 缓存和数据库一致性问题,看这篇就够了
  • 5 个正在爆火的开源AI工具
  • 狗东面试,起手就问 MVCC 原理
  • Anthropic报告:AI对就业的影响
  • OpenFeign 夺命连环 9问,又挂这上了
  • 68个适合个人GPU部署的LLM
  • C++ vector、unordered_set和稀疏集的增删遍历性能对比 - 码客
  • 啪!啪!@Transactional 注解的12种失效场景,这坑我踩个遍
  • 第8篇:PI控制器设计实战演练
  • Day10 | 用栈实现队列、用队列实现栈、有效的括号、删除字符串中的所有相邻重复项
  • 3.12笔记
  • 华为CE6800交换机堆叠配置案例
  • 【AI总结博客】编码者卢布 技术博客深度分析 ---- 借助腾讯WorkBuddy得出的分析结果
  • 调试线程应用程序
  • 5. 最长回文子串
  • L2-025 分而治之