Java中的定时任务

 现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了。

很多业务需求的实现都离不开定时任务,例如,每月一号,移动将清空你上月未用完流量,重置套餐流量,以及备忘录提醒、闹钟等功能。

Java 系统中主要有三种方式来实现定时任务:

  • Timer和TimerTask
  • ScheduledExecutorService
  • 三方框架 Quartz

下面我们一个个来看。

Timer和TimerTask

先看一个小 demo,接着我们再来分析其中原理:

image

这种方式的定时任务主要用到两个类,Timer 和 TimerTask。其中,TimerTask 继承接口 Runnable,抽象的描述一种任务类型,我们只要重写实现它的 run 方法就可以实现自定义任务。

而 Timer 就是用于定时任务调度的核心类,demo 中我们调用其 schedule 并指定延时 1000 毫秒,所以上述代码会在一秒钟后完成打印操作,接着程序结束。

那么,使用上很简单,两个步骤即可,但是其中的实现逻辑是怎样的呢?

Timer 接口

首先,Timer 接口中,这两个字段是非常核心重要的:

image

TaskQueue 是一个队列,内部由动态数组实现的最小堆结构,换句话说,它是一个优先级队列。而优先级参考下一次执行时间,越快执行的越排在前面,这一点我们回头再研究。

接着,这个 TimerThread 类其实是 Timer 的一个内部类,它继承了 Thread 并重写了其 run 方法,该线程实例将在构建 Timer 实例的时候被启动。

run 方法内部会循环的从队列中取任务,如果没有就阻塞自己,而当我们成功的向队列中添加了定时任务,也会尝试唤醒该线程。

我们也来看一下 Timer 的构造方法:

public Timer(String name) {     thread.setName(name);     thread.start(); }

再简单不过的构造函数了,为内部线程设置线程名,并启动该线程。

最后,我们着重看一下 Timer 中用于配置一个定时任务进任务队列的方法。

//在时刻 time 处执行任务 schedule(TimerTask task, Date time)  //延时 delay 毫秒后执行任务 schedule(TimerTask task, long delay)  //固定延时重复执行,firstTime为首次执行时间, //往后没间隔 period 毫秒执行一次 schedule(TimerTask task, Date firstTime, long period)  //固定延时重复执行 //首次执行时间为当前时间延时 delay 毫秒 schedule(TimerTask task, long delay, long period)  //固定频率重复执行,每过 period 毫秒执行一次 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)  //固定频率重复执行 scheduleAtFixedRate(TimerTask task, long delay, long period)

相信有了注释,这几个方法的区别与作用应该不难理解,但是其中有两个概念需要作一点区分。

==固定延时== VS ==固定频率==

固定延时:以任务的上一次 实际 执行时间做参考,往后延时 period 毫秒。

固定频率:任务的往后每一次执行时间都在任务提交的那一刻得到了确定,不论你上次任务是否意外延时了,定时定点执行下一次任务。

这两者的区别还是很大的,希望你能够理解清楚,接着我们以其中一个方法为例,看看底层实现。

以这个方法为例,其他重载方法的底层调用都是同样的,我们不去赘述。

image

这个方法的作用,我们再说一遍。

以当前时间为准,延时 delay 毫秒后第一次执行该任务,并且采取固定延时的方式,每隔 period 毫秒再次执行该任务。

开头的两个异常判断我们不再赘述,看看 sched 方法:

image

方法需要传入三个参数,参数 task 代表的需要执行的任务体,TimerTask 我们回头会详细介绍,这里你知道它代表了一个任务体即可。

参数 time 描述了该任务下一次执行的时刻,计算机底层是以毫秒描述时刻的,所以这里转换为 long 类型来描述时刻。

参数 period 是固定延时的毫秒数。

整个方法的逻辑我们可以总结概括一下,具体的代码就不一行行分析了,因为也不难。

  1. 首先使用任务队列的内置对象锁,锁住个队列。
  2. 接着再去锁住我们的 task,并修改其内部的一些属性字段值,nextExecutionTime 指明下一次任务执行时间,period 设置固定延时的毫秒数,修改 state 状态为计划中。
  3. 然后将 task 添加到任务队列,其中 add 方法内部会进行最小堆重构,参考的就是 nextExecutionTime 字段的值,越小优先级越高。
  4. 判断如果自己就是队列第一个任务,那么将唤醒 Timer 中阻塞了的任务线程。

可能会有人疑问,Timer 如何判断一个任务是否是重复执行的,还是单次执行就结束的?

答案在 TimerThread 的 run 方法里,有兴趣你可以去研究下,方法体比较多比较长,这里不做分析。

当我们构造 Timer 实例的时候,就会启动该线程,该线程会在一个死循环中尝试从任务队列上获取任务,如果成功获取就执行该任务并在执行结束之后做一个判断。

如果 period 值为零,则说明这是一次普通任务,执行结束后将从队列首部移除该任务。

如果 period 为负值,则说明这是一次固定延时的任务,修改它下次执行时间 nextExecutionTime 为当前时间减去 period,重构任务队列。

如果 period 为正数,则说明这是一次固定频率的任务,修改它下次执行时间为 上次执行时间加上 period,并重构任务队列。

其实,我也已经把 TimerThread 的 run 方法里最核心的逻辑也已经介绍了,建议大家亲自去研究研究具体代码的实现,你会对这一块的逻辑更清晰。

关键字:
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信