avatar

Java定时任务 一 Timer及TimerTask的案例解析及源码分析

一、概述

  定时任务这个概念在Java的学习以及项目的开发中并不陌生,应用场景也是多种多样。比如我们会注意到12306网站在锁定车票的30分钟以内要进行车票费用的支付,否则订单会被取消;再比如我们的考试系统,考试管理人员上传好考试信息,考试系统会根据考试信息准时开考;还有上课的铃声会在每周的上课时间准时响起,等等。这些生活细节只要我们稍微注意,留心观察,便会发现编程已经早早地融入到我们生活的方方面面,定时任务也随之应用到这些程序之中。因此,了解好定时任务,对我们的程序的编写有很大的帮助。

二、使用案例

  在Java语言中实现定时任务这个功能,我们可以使用Java提供的Timer以及TimerTask来进行简单的实现,jdk1.5之后也可以使用ScheduledExcecutorService 进行实现,后续的文章(定时任务(二))会介绍这种实现方式,并会在定时任务(三)(最后一篇)中介绍一个常见的应用场景中定时任务的实现案例。
  这里实现的案例是: 指定一个日期,并间隔一定的时间,周期性的执行一个任务,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TimerTaskTest {
public static void main(String[] args) throws ParseException {
//创建定时任务
TaskTest taskTest1 = new TaskTest("taskTest1");
//定时器创建
Timer timer = new Timer("定时器线程");
//指定开始执行的日期
Date startDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-03-03 10:55:10");
//时间间隔,单位是ms
long intervalTime = 2 * 1000;
try {
timer.schedule(taskTest1, startDate,intervalTime);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class TaskTest extends TimerTask{
// 测试任务名称
private String taskTestName;
public TaskTest(String taskTestName) {
super();
this.taskTestName = taskTestName;
}
@Override
public void run() {
//这里是需要执行的具体任务
System.out.printf("定时任务 %s 执行中,响铃响一下\n",taskTestName);
}
}

三、实现原理分析

  上面实现的定时任务,主要用到两个类,一个是Timer类,一个是TimerTask类。Timer类主要用于维护定时任务,管理任务的执行,TimerTask是具体执行任务的类。Timer类的schedule方法可以设置任务的运行时机,在上面的案例中,运行方式为:从startDate时间点开始,每隔intervalTime时间重复执行,执行的任务是taskTest1;TaskTest这个类继承自TimerTask,并重写了run方法,我们需要实际运行的任务代码就是放在这里。
  Timer是定时器类,当实例化一个Timer对象的时候,这时新建了一个TaskQueue任务队列并且新建了一个线程;任务队列主要存放待执行的任务,线程主要负责管理待执行的任务。TimerThread线程需要以queue为参数进行构造,如下图:
Timer_1
Timer_2
  TaskQueue是任务队列,其实现方式是new了一个TimerTask数组,当调用schedule的时候,是向这个数组里面添加元素,然后根据执行时间的先后对数组元素进行排序,从而确定最先开始执行的任务,如下图:
TaskQueue
  开启的TimerThread线程,会执行run方法里面的mainLoop函数,这个函数会对TaskQueue队列的首元素进行判断,看是否达到执行时间,如果没有,则进行休眠,休眠时间为队首任务的开始执行时间到当前时间的时间差。每当timer对象调用schedule方法时,都会向队列添加元素,并唤醒TaskQueue队列上的线程,这时候TimerThread会被唤醒,继续执行mainLoop方法。如下图所示:
mainLoop_1
mainLoop_2
  mainLoop函数源代码如下,函数执行的是一个死循环,并且加了queue锁,从而保证是线程安全的。通过queue.getMin()找到任务队列中执行时间最早的元素,然后判断元素的state,period,nextExecutionTime,SCHEDULED等属性,从而确定任务是否可执行。主要是判断这几个属性:1,state 属性,如果为取消(即我们调用了timer的cancel方法取消了某一任务),则会从队列中删除这个元素,然后继续循环;2,period 属性,如果为单次执行,这个值为0,周期执行的话,为我们传入的intervalTime值,如果为0,则会移出队列,并设置任务状态为已执行,然后下面的 task.run()会执行任务,如果这个值不为0,则会修正队列,设置这个任务的再一次执行时间,queue.rescheduleMin这个函数来完成的这个操作;3,taskFired 属性, 如果 executionTime<=currentTime 则设置为true,可以执行,否则线程就会进行休眠,休眠时间为两者之差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

四、总结

  Java中的Timer和TimerTask可以实现基础的定时任务, 他的实现主要用到了线程和队列的组合,通过对timer源码的学习,能更进一步的了解线程和队列方面的知识。在实际的项目中,出于性能和资源的考虑,我们可能不会这么简单粗暴的使用这种方式来实现我们需要的定时功能,原因在于,当我们需要定时执行的任务很多的时候,这种方式会生成很多的线程出来,这是很消耗资源的,同时线程的调度也是很占资源的。因此,后面的博客会介绍其他的实现定时任务的方式,通过线程池的方式管理线程,提高线程利用率,从而提高效率。
  欢迎一起学习交流,如有不足之处,欢迎下方留言讨论。

文章作者: IT之旅
文章链接: https://www.meijindong.top/posts/2679391756.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 程序员在旅途
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论