线程池里的代码明明报错了,为什么控制台一行异常日志都不打?
昨天下午运营说有个用户标签更新任务没跑后台数据全是旧的这个任务我前两天才优化过逻辑很简单就是从数据库查一批人算一下标签再写回去。为了快点我还特意用了线程池做并发。我第一时间去翻服务器的日志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对象。我们来看看FutureTask的run()方法里到底干了什么JDK 8 源码片段public void run() { // ... 省略状态检查 ... try { CallableV 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 { // ... } }真相大白你的业务代码抛出的异常在catch (Throwable ex)里被捕获了。它并没有被打印到控制台也没有被抛给Thread的UncaughtExceptionHandler。它被传递给了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.coloradmin.cn/o/2414209.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!