点击上方「蓝字」关注我们

问题描述

程序发版之后一个定时任务突然挂了!

“幸亏是用灰度跑的,不然完蛋了。????”

之前因为在线程池踩过坑,阅读过ThreadPoolExecutor的源码,自以为不会再踩坑,没想到又一不小心踩坑了,只不过这次的坑踩在了ScheduledThreadPoolExecutor上面。写代码真的是要注意细节上的东西。

ScheduledThreadPoolExecutorThreadPoolExecutor功能的延伸(继承关系),按照以前的经验,很快就知道的问题所在,特此记录一下。希望小伙伴们别重蹈覆辙。

问题重现

代码模拟:

public class ScheduledExecutorTest {
private static LongAdder longAdder = new LongAdder();
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleAtFixedRate(ThreadExecutorExample::doTask,
1, 1, TimeUnit.SECONDS);
}
private static void doTask() {
int count = longAdder.intValue();
longAdder.increment();
System.out.println("定时任务开始执行 === " + count);
// ① 下面这一段注释前和注释后的区别
if (count == 3) {
throw new RuntimeException("some runtime exception");
}
}
}

代码块①注释的情况下,执行结果:

定时任务开始执行 === 0
定时任务开始执行 === 1
定时任务开始执行 === 2
定时任务开始执行 === 3
定时任务开始执行 === 4
定时任务开始执行 === 5
定时任务开始执行 === 6
定时任务开始执行 === 7
定时任务开始执行 === 8
.... 会一直执行下去

代码块①不注释的情况下,执行结果:

定时任务开始执行 === 0
定时任务开始执行 === 1
定时任务开始执行 === 2
定时任务开始执行 === 3
// 停止输出,任务不再被执行

初步结论

因为任务最外面没有用try-catch 捕捉,或者说任务执行时,遇到了 Uncaught Exception,所以导致这个定时任务停止执行了。

走进源码看问题

有了初步的结论,我们需要知道的就是,ScheduledExecutorService这个定时线程调度器(定时任务线程池)在碰到 Uncaught Exception 的时候,是怎么处理的,是在哪一块导致任务停止的?

之前是看过ThreadPoolExecutor的源码,当线程池的线程工作时抛出 Uncaught Exception 时,会这个线程抛弃掉,然后再新启一个worker,来执行任务。在这里显然不一样,因为这个问题的主体是定时任务,定时任务的后续执行停止了,而不是worker线程。

带着问题,我们走进源码去看更深层次的答案。

这里说一句,本文不会成为ScheduledThreadPoolExecutor的完整源码解析,只是在具体问题场景下,讨论源码的运行。

ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();

先看生成的ScheduledExecutorService实例,

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}

返回了一个DelegatedScheduledExecutorService对象,

static class DelegatedScheduledExecutorService
extends DelegatedExecutorService
implements ScheduledExecutorService {
private final ScheduledExecutorService e;
DelegatedScheduledExecutorService(ScheduledExecutorService executor) {
super(executor);
e = executor;
}
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
return e.schedule(command, delay, unit);
}
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
return e.schedule(callable, delay, unit);
}
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
return e.scheduleAtFixedRate(command, initialDelay, period, unit);
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return e.scheduleWithFixedDelay(command, initialDelay, delay, unit);
}
}

发现这个类实际上就是把ScheduledExecutorService 包装了一层,实际上的动作是由ScheduledThreadPoolExecutor类执行的。

所以我们再进去看,这里我们关注的scheduleAtFixedRate(...)方法,也就是计划执行定时任务的方法。

我们先不急着看方法的实现,先看下它的接口层ScheduledExecutorService,这个方法的 JavaDoc 上面写了这么一段话:

If any execution of the task encounters an exception, subsequent executions are suppressed.
Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.

如果任务的任何一次执行遇到异常,则将禁止后续执行。其他情况下,任务将仅通过取消操作或终止线程池来停止。

如果某一次的执行时间超过了任务的间隔时间,后续任务会等当前这次执行结束才执行。

这个方法的注释,已经告诉我们了在使用这个方法的时候,要注意的事项了。

  1. 要注意发生异常时,任务终止的情况。

  2. 要注意定时任务调度会等待正在执行的任务结束,才会发起下一轮调度,即使超过了间隔时间。

这里说一句,线程池的使用中,注释真的十分关键,把坑说的很清楚。(mdzz,说了那么多你自己还不是没看????????)

这个注释已经解释了一大半,但是我们这个是源码解析,当然看看里面是怎么做的,

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
// ①
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}

这里的核心逻辑就是将 Runnable 包装成了一个ScheduledFutureTask对象,这个包装是在FutureTask基础上增加了定时调度需要的一些数据。(FutureTask是线程池的核心类之一)

decorateTask是一个钩子方法,用来给扩展用的,在这里的默认实现就是返回ScheduledFutureTask本身。

然后主逻辑就是通过delayedExecute放入队列中。(这里省略对源码中线程池shutdown情况处理的解释)


这里我们放一张图,简单描述一下ScheduledThreadPoolExecutor工作的过程:

我们很容易都推断出来,我们想要找的对于 Uncaught Exception 逻辑的处理肯定是在任务执行的时候,从哪里可以看出来呢,就是ScheduledFutureTaskrun方法。

public void run() {
// 是否是周期性任务
boolean periodic = isPeriodic();
// 如果不可以在当前状态下运行,就取消任务(将这个任务的状态设置为CANCELLED)。
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
// 如果不是周期性的任务,调用 FutureTask # run 方法
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
// 如果是周期性的。// 执行任务,但不设置返回值,成功后返回 true。
// 设置下次执行时间
setNextRunTime();
// 再次将任务添加到队列中
reExecutePeriodic(outerTask);
}
}

这里我们关注的是ScheduledFutureTask.super.runAndReset(),实际上调用的是其父类FutureTask

runAndReset()方法,这个方法会在执行成功之后重置线程状态,reset就是这个语义。

可以看到,当上述方法执行返回false的时候,就不会再次将任务添加的队列中,这和我们最开始看到的异常情况是一致的,看来答案就在这个方法里面。那我们接下去看看。

protected boolean runAndReset() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return false;
boolean ran = false;
int s = state;
try {
Callable<V> c = callable;
if (c != null && s == NEW) {
try {
// ① 任务执行
c.call(); // don't set result
ran = true;
} catch (Throwable ex) {
setException(ex);
}
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
//
return ran && s == NEW;
}

代码块①是执行任务的地方,这里有一个默认为false的ran变量,当任务执行成功时,ran会被设成 true,即任务已执行。可以看到当代码块①抛出异常的时候,ran 等于false,runAndReset()返回给调用方的最终结果是false,也就应验了我们上面说的逻辑走向。

总结

整篇文章到这里结束啦,本篇主要介绍了当ScheduledThreadPoolExecutor碰到 Uncaught Exception 时的源码处理逻辑。我们自己在使用这个线程池时,需要注意对任务运行时异常的处理(最简单的方式就是在最外层加个try-catch ,然后捕捉打印日志)。

有你想看的精彩

LOL钓鱼网站实战渗透
边缘计算  一文简单读懂
Tomcat是如何运行的?整体架构又是怎样的?

支持百亿级别的 Java 分布式日志组件EasyLog

戳这儿

有个定时任务突然不执行了,别急,原因可能在这相关推荐

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

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

  2. spring定时任务执行两次的原因与解决方法

    spring定时任务执行两次的原因与解决方法 参考文章: (1)spring定时任务执行两次的原因与解决方法 (2)https://www.cnblogs.com/yolanda-lee/p/7339 ...

  3. java实现每天定时执行任务,Spring Task定时任务每天零点执行一次的操作

    最近根据项目的需求,需要限制用户每天的发送短信数量.这样以来就需要写一个定时任务,每天去置零一次所有用户的发送短信统计数量. 首先,在application.xml文件中添加 接着就是编写自己的业务处 ...

  4. DataX踩坑2 | 定时任务crontab不执行或报错:/bin/sh: java: command not found

    前面两天写了一个DataX的增量同步脚本,今天检查了一下发现定时任务没有执行成功,数据并没有同步.以下为排查问题和解决方法. 一.定时任务crontab不执行 脚本(测试用的可以设为每分钟一次:*/1 ...

  5. quartz定时任务突然不执行了

    高并发情况下,quartz定时任务突然不执行了, 背景: 整个项目架构,高并发操作比较多, 有多个线程池,周期线程池,和定时任务,占用多个资源 导致现场出现定时任务走一段时间就不走的情况 当前定时任务 ...

  6. 定时任务每秒执行、每分钟执行、每小时执行、每天执行、每周执行、每月执行、每年执行、定时任务重复执行、循环执行

    在平时生活.系统运维.实验室.学校等场景下,有很多定期循环执行任务的需求.比如:在系统运维过程中,常常会在每天凌晨1点以后执行某些批处理脚本:在实验室做实验过程中,需要每隔10分钟去操作一下设备等等. ...

  7. struts2 ajax请求发现执行action两次原因

    struts2中使用json插件执行ajax处理时,如果方法名是get方法的时候,方法会莫名其妙的执行两次. 原因: struts2 中JSON的原理是在ACTION中的get方法都会序列化,所以前面 ...

  8. IS审计师执行风险评估的主要原因

    为了让学员能够更好的遇见更多更好的老师,与更多的小伙伴一起共同进步,互通有无提高学习兴趣,增加沟通和互动,麟学堂-网络空间安全学习群每周二.周五晚上8点准时都有答题抢红包活动,寓教于乐. 我们群内之间 ...

  9. 定时任务重启后执行策略_quartz定时任务框架调度机制解析

    quartz2.2.1集群调度机制调研及源码分析 引言 quartz集群架构 调度器实例化 调度过程 触发器的获取 触发trigger: Job执行过程: 总结: 附: 引言 quratz是目前最为成 ...

最新文章

  1. 四十六、文件系统的层次结构
  2. .net项目文档生成工具使用
  3. oracle audit for 11g
  4. 字符设备驱动开发流程详解
  5. python哪里下载import包-python import 自己的包
  6. CSDN Github Markdown编辑常用功能符号补充
  7. mysql授权用户主机_MySQL用户授权(GRANT)
  8. Python多线程编程基础1:为什么要使用线程
  9. 唐山师范学院计算机科学与技术地址,2021年唐山师范学院有几个校区,大一新生在哪个校区...
  10. jsp页面播放服务器视频
  11. 都说苹果秋季发布会像一杯白开水,那么...
  12. 纯C语言编程-游戏之弹跳球
  13. uniapp app 端打开pdf文件方式
  14. 微信视频号打造带货闭环:主播叫苦连天
  15. 新兴网站神秘虎嗅,获得数百万元投资
  16. 3个老鼠确定8个瓶子哪瓶有毒
  17. Nginx 的安装配置
  18. 可优化-PAT (Basic Level) Practice Python解法 1026 程序运行时间(时间进位/四舍五入Tobe解决)
  19. 我用Python爬取了妹子网200G的套图
  20. 862计算机学科综合(非专业),2018年北京市培养单位862计算机学科综合(非专业)之计算机操作系统考研基础五套测试题...

热门文章

  1. jquery中的event
  2. 飞腾计算机硬件测试方法,银河麒麟操作系统
  3. 椭圆形方程的差分解法
  4. 网易笔试题:混合颜料
  5. Pytorch 学习 (一)Minst手写数字识别(含特定函数解析)
  6. AI时代的稀缺人才:全面剖析数据科学家成长的4个阶段
  7. r5处理器_AMD和Intel之争,用数据告诉你:i5和R5两款千元处理器该怎么选
  8. camera face
  9. 银河麒麟操作系统使用
  10. GO学习笔记:struct的匿名字段