2018年12月12日18:44:53

案件现场

不久前,在开发改造公司一个端到端监控日志系统的时候,出现了一个bug:有个扫表写日志的线程无故挂掉。

顺藤摸瓜

我看了很久的代码,都没有想出来有什么地方有逻辑问题。万金油的方法是,重启。当我满心欢喜地认为重启是个好方法的时候,问题又重现了。

我有点无奈地看着自己的代码

本我:堪称完美的逻辑,还有什么地方是我没有注意到的吗?
真我:当然有了,你这个菜鸟,你不知道的地方多着呢。

于是,去找老大问一下问题怎么解决,老大说去生产数据库上导十万数据到测试库,然后在本地debug一下。接着,我就从数据库里面导出一万数据开始测试,在eclipse启动进程,日志写在本地文件。很快,问题再一次出现。然后断点,然后找到出问题的地方。出问题的地方如下:

代码就一行:

String timeStamp = DateUtil.str2Date(receiveTime, DateUtil.YYYYMMDDHH24MISS).getTime() + "000";

这行代码的意思是,将字符串的接收时间receiveTime格式化,getTime()得到时间戳,因为格式是要微秒,加了三个零。

DateUtil.str2Date方法:(String时间转化为Date类型,关于时间转换可以看看本人的String、Date和Timestamp的互转)

public static Date str2Date(String dateStr, String dateFormat){if (StringHelper.isEmpty(dateStr)) {return null;}SimpleDateFormat df = new SimpleDateFormat(dateFormat);try {return df.parse(dateStr);} catch (Exception ex) {return null;}
}

这个工具类当parse方法抛出异常的时候返回null,看起来是没有问题的,但是我在转换之后没有判断是否为空即null,然后就变成了null.getTime(),接着就抛了一个很常见的NullPointerException异常。

到这里,看似问题已经解决了,但是问题并没有那么简单。

寻根问底

上面说到的在线程中抛出了NullPointerException异常,解决方法是增加一个判断是否为空的条件就可以了。但是一般来说,有异常的时候,程序没有捕获异常,日志里或者debug时控制台会打印异常信息,类似这种:

at com.netease.backend.rds.task.CleanHandleThread.run(CleanHandleThread.java:65)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:439)
at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:317)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:150)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(ScheduledThreadPoolExecutor.java:98)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:204)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)

但实际上我debug的时候,并没有看到打印的异常信息。我是断点到这一步,发现下一行代码没有执行,我就断定问题是在这里,而且空指针异常一下子就能看出来了。问题来了,为什么没有打印异常信息呢?我想应该是线程的问题,代码里启动这个写日志的定时任务用的是ScheduledExecutorService:

我Google了一下,发现其实有很多前辈都曾遇到过这个问题。

在这些文章中,我找到了我要的答案。我引用其中的一篇文章从一个java线程挂掉的例子讨论分析定位问题基本原则文字作为答案吧。

那么,Java线程挂掉的主要原因是:Any thrown exception or error reaching the executor causes the executor(ScheduledExecutorService) to halt. No more invocations on the Runnable, no more work done. This work stoppage happens silently, you’ll not be informed.
也就是说,如果使用者抛出异常,ScheduledExecutorService 将会停止线程的运行,而且不会报错,没有任何提示信息。

这就是在日志中和控制台都没有看到打印异常信息的原因。

解决方法

写了一个测试类,有兴趣可以研究一下这个bug。

public class ScheduledExecutorServiceThrowExceptionTest {private static int i = 0;public static void main(String[] args) {ScheduledExecutorService exc = Executors.newSingleThreadScheduledExecutor();exc.scheduleAtFixedRate(new Runnable(){@Overridepublic void run() {i++;if (i==6) {throw new RuntimeException();} else {System.out.println(i);}}}, 0, 1, TimeUnit.SECONDS);}}

测试结果是:

结果显示,当程序抛出异常的时候,线程就不再运行了,也就是挂了。

解决方法:

  • 1、直接加一个try-catch进行异常捕获,然后你可以打印你需要的异常信息或者处理异常。
public class ScheduledExecutorServiceThrowExceptionTest1 {private static int i = 0;public static void main(String[] args) {ScheduledExecutorService exc = Executors.newSingleThreadScheduledExecutor();exc.scheduleAtFixedRate(new Runnable(){@Overridepublic void run() {try {// doSomething();// TODO:具体业务逻辑i++;if (i==6) {throw new RuntimeException();} else {System.out.print(i + " ");}} catch (Exception ex) {System.out.println();System.out.println("在ScheduledExecutorService中有异常抛出,异常堆栈:" + ex.getStackTrace());}}}, 0, 1, TimeUnit.SECONDS);}}

结果是打印了异常信息,且线程没有被中断。

1 2 3 4 5
在ScheduledExecutorService中有异常抛出,异常堆栈:[Ljava.lang.StackTraceElement;@1bb53ed8
7 8 9 10 11 12 13

  • 2、通过ScheduledFuture对象返回异常信息
public class ScheduledExecutorServiceThrowExceptionTest2 {private static int i = 0;public static void main(String[] args) {ScheduledExecutorService exc = Executors.newSingleThreadScheduledExecutor();ScheduledFuture<?> handle = exc.scheduleAtFixedRate(new Runnable(){@Overridepublic void run() {i++;if (i==6) {throw new RuntimeException();} else {System.out.print(i + " ");}}}, 0, 1, TimeUnit.SECONDS);try {handle.get();} catch(Exception ex) {System.out.println();System.out.println("在ScheduledExecutorService中有异常抛出,异常堆栈:" + ex.getStackTrace());}}}

这个解决方法打印了异常信息,但是并没有阻止线程挂掉。

1 2 3 4 5
在ScheduledExecutorService中有异常抛出,异常堆栈:[Ljava.lang.StackTraceElement;@33909752

总结

一个ScheduledExecutorService启动的Java线程无故挂掉的原因是:如果使用者抛出异常,ScheduledExecutorService 将会停止线程的运行,而且不会报错,没有任何提示信息。解决方法是:try-catch将异常信息打印,或者用ScheduledFuture<?>获取线程运行结果。

写的bug多,自然经验就多了,但是要注意总结。

2018年12月13日09:08:19


由于本人水平有限,如果本文有什么错漏,请不吝赐教
感谢阅读,如果您觉得本文对你有帮助的话,可以点个赞同

scheduledexecutorservice 只执行一次_一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考...相关推荐

  1. scheduled线程池ScheduledExecutorService只执行一次_有个定时任务突然不执行了

    scheduled线程池ScheduledExecutorService只执行一次_有个定时任务突然不执行了 原因 If any execution of the task encounters an ...

  2. java定时器只执行一次_搞定Java并发:为什么说只有1种实现线程的方法?(下)...

    在 上一篇(戳下看前情) 中,徐大带我们详细论证了,所有实现线程的方式归根结底就是基于 Runnable 接口或继承 Thread 类.接下来,请徐大继续解释,为什么说这两种方式本质上是一种. 一. ...

  3. winform 让他间隔一段时间 执行事件 且只执行一次_记一次golang定时器引发的诡异错误...

    作为一只在9127工作制下摸鱼的程序猿,周六自然是愉快的加班了.一早上除了一位新同学在我们的"敏捷迭代"下错删了接口之外没什么大事. 临近中午,突然隔壁组大佬找到我,表示有个go语 ...

  4. java fields是_一个快速生成R2.java中fields的插件

    一个快速生成R2.java中fields的插件 项目地址:github.com/JeasonWong/- 介绍 在子 module 中使用 ButterKnife 时,如果想使用 ButterKnif ...

  5. 为什么线程池里的方法会执行两次_面试官问你java都有哪些线程池,自己是否自定义过线程池...

    我还记得大学实习面试时,被问到什么是线程池这个问题,因为这个题我被录取了,原因就是我背出来了,而另外一个面试的没背出来,说实话当时还真不知道它是干什么的,就是看面试题给背下来了,在之后就是在实际开发中 ...

  6. java方法用泛函_一个关于泛函分析和Java语言的千古谜团 - 水木快照 JinghuaSoft

    发信人: Sunyata (塑造人类灵魂工程师的工程师), 信区: Mathematics 标  题: 一个关于泛函分析和Java语言的千古谜团 发信站: 水木社区 (Mon Mar  9 10:49 ...

  7. java lock 效率_工作常用4种Java线程锁的特点,性能比较、使用场景

    多线程的缘由 在出现了进程之后,操作系统的性能得到了大大的提升.虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对实时性有了要求. 使用多线程的理由之一是和进程相比,它是一种非常花 ...

  8. 如何停止一个正在运行的java线程

    与此问题相关的内容主要涉及三部分:已废弃的Thread.stop().迷惑的thread.interrupt系列.最佳实践Shared Variable. 已废弃的Thread.stop() @Dep ...

  9. 一个 Java 线程池bug引发的 GC 机制思考

    本文作者:空无 原文链接:https://segmentfault.com/a/1190000021109130 问题描述 前几天,在帮同事排查一个线上偶发的线程池错误 逻辑很简单,线程池执行了一个带 ...

最新文章

  1. Ello讲述Haar人脸检测:易懂、很详细、值得 ...
  2. mysql 外键有啥用途_Mysql外键是什么?有哪些用处?(图文+视频)
  3. C#实现局域网UDP广播--
  4. 上周五,小编参加了一场高大上的“9”会
  5. oracle查看物化视图的索引,oracle – 物化视图中的域索引返回零行
  6. SpringCloud Ribbon中的7种负载均衡策略!
  7. 黑客秘笈-渗透测试实用指南 第三版
  8. TFLite基础知识
  9. 从RDS中获取binlog
  10. Hexo博客系列(三)-将Hexo v3.x个人博客发布到GitLab Pages
  11. RNN,LSTM中如何使用TimeDistributed包装层,代码示例
  12. mysql批量插入优化
  13. Error while building/deploying project QTtest (kit: Desktop Qt 5.8.0 MinGW 32bit)
  14. 华为手机怎么使用新系统鸿蒙,华为手机鸿蒙系统如何退回EMU
  15. 大数据 排错日记0004——Unable to check if JNs are ready for formatting
  16. word两幅图并排并且插入题注不会乱
  17. 中电药明招募资深Python开发工程师
  18. 棋盘覆盖算法java_棋盘覆盖问题(算法分析)(Java版)
  19. c语言中 float delta,比较float和double值与delta吗?
  20. 【强化学习】强化学习数学基础:Actor-Critic方法

热门文章

  1. 深入浅出Java核心技术开篇(总结)
  2. StringBuffer类的功能
  3. openstack 手动安装版 功能测试
  4. 后续:为LAMP添加XCache加速。
  5. vrrp路由协议实验
  6. flex3+blazeds+spring+hibernate整合小结
  7. async/await
  8. 选择嵌套_还不会if函数的嵌套判断,学会这方法,就跟复制粘贴一样简单
  9. python爬虫 被重定向_爬虫篇 | 认识Python最最最常用语重要的库Requests
  10. Web前端——JavaScript(基本语法)