java 定时任务之Timer

Wesley13
• 阅读 581

java 定时任务之Timer

Timer是什么?

Timer位于java.util包中。官方API的描述是用来控制任务执行的,每个任务可以执行一次,也可以执行多次。在实际应用中,我们可以用它来控制某个任务在特定的时间执行,或者按照某个固定频率或者时间间隔执行。

##Timer怎么用? 它提供了三类方法:

  1. schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务。task只执行一次
  2. schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复固定延迟执行。
  3. scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复固定频率执行。

Timer使用应该注意哪些问题?

1. Timer对于任务的调度是单线程的。

单线程意味着当存在多个任务需要被调度执行的时候就必须按顺序执行。

2.Timer中的任务应该能够快速完成。

由于Timer是单线程调度,所以如果某个任务执行时间过长,后面等待执行的任务就会被延迟。

3.Timer中“按照固定延迟”和“按照固定频率”两种执行方式是有区别的。

Timer中的schedule(TimerTask task, Date firstTime, long period) 方法是按照固定的延迟(period)来重复执行任务的,而scheduleAtFixedRate(TimerTask task, Date firstTime, long period)则是按照固定频率(Rate)来重复执行任务的。

固定延迟:是以当前程序运行的时间(System.currentTimeMillis())开始,每过固定时间间隔就执行一次。 固定频率:是以参数中的firstTime(第二个参数)开始,每过固定时间间隔就执行一次。

举个例子:

//TimerTask实现类,TimerTask实现了Runnable。
class MyTimerTask extends TimerTask {
    @Override
    public void run() {
        System.out.println("Run my timer task");
    }
}

//main方法
public static void main(String[] args) {
    Timer timer = new Timer();
    // 按照固定延迟(20分钟)重复执行
    timer.schedule(new MyTimerTask(), formatDate("2015-10-01 23:40:00"), 20 * 60 * 1000);
    // 按照固定速率(20分钟)重复执行
    timer.scheduleAtFixedRate(new MyTimerTask(), formatDate("2015-10-01 23:40:00"), 20 * 60 * 1000);
}

可以看到,两个方法的开始执行时间都是2015-10-01 23:00:00,为了说明两者的不同,我们选择在现在,2015年10月1日23点10分执行。那么,它们连续三次的执行时间应该是如下表所示:
| 执行方式 | 第一次 | 第二次 | 第三次 |
| :--------: |:--------:|:--------:|:--------:|
| 固定延迟 | 2015-10-01 23:10:00 |2015-10-01 23:30:00|2015-10-01 23:50:00 |
| 固定频率 |2015-10-01 23:00:00| 2015-10-01 23:20:00 |2015-10-01 23:40:00 |

说明 上表的时间是不准确的,只是为了阐明两者的不同,大家不必较真。实际上,由于任务执行本身会耗时,所以以固定延迟方式执行的任务的执行时间与上表会有一些出入,后面会详细介绍。 但是以固定频率的,任务的执行时间是没有问题的。但是固定频率的由于第一次执行的时间已经比程序运行时间要早,所以会立马执行,然后在23:20:00的时候执行第二次。

Timer是如何实现的?

你会如何实现?

先思考一下:如果让我来实现一个类似于Timer的任务调度,我会如何实现?需要从哪几方面考虑?PS:不使用java提供的现成的,自己写程序实现。

首先,我们需要一个数据结构保存用户提交的任务。

数据结构有很多,List,Map,Set,Queue,Array,Stack等等。 ####其次,我们应该能够及时执行用户提交的任务。 我能想到的:写个死循环,一直从列表中读取任务,判断任务的执行时间与当前时间是否一致,如果到了执行时间就执行,否则就读取下一个任务判断。有人可能会说用观察者模式,给schedual方法增加监听器,当提交任务的时候进行相应的任务判断,如果任务一定能执行是可以的,但是如果任务没有到达执行时间,就需要后续再次判断,所以达不到目的。

想了这么多,让我们来看看JDK中是如何实现的吧。

JDK中是如何实现的?

1.如何保存用户提交的任务?

public class Timer {
    //用来保存用户提交的任务
    private final TaskQueue queue = new TaskQueue();
    //其余部分略
}

通过查看源码我们可以看到,Timer中用到了一个TaskQueue(任务队列)来保存用户提交的任务。 TimerQueue内部通过数组来保存TimerTask,数组保存着一个小根堆结构,即执行时间最早的任务放在队首,在每次新增或删除任务的时候,都会根据任务的执行时间进行堆排序,重新形成小根堆,以保证每次从队列中取任务都能取到最紧急的那个任务。 它提供了getMin()方法,用于获取队列中最紧急的任务,add()方法用于向队列中添加任务,rescheduleMin()方法用于将队列中第一个任务设置成当前最紧急的任务。

2.如何控制任务的执行?

通过构造函数我们可以看到,在Timer启动的时候,会在内部启动一个TimerThread,该类是Timer的一个内部类。启动代码如下:

/**
 * The timer thread.
 */
private final TimerThread thread = new TimerThread(queue);
public Timer(String name) {
        thread.setName(name);
        thread.start();
}

TimerThread很简单,实现了Runnable接口,run方法里面是一个循环,程序一直循环,从任务队列queue中获取任务然后执行,TimerThread代码如下:

class TimerThread extends Thread {
    public void run() {
        try {
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }
}

可以看到,其run方法中只有一个mainLoop(),代码如下:

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) {
        }
    }
}

mainLoop()方法的主要作用就是一直循环,不停的从任务队列中获取任务,判断任务是否满足执行条件,如果满足就执行任务。其基本过程如下:

  1. 从任务队列里面获取一个执行时间最接近当前时间(即任务队列中执行时间最早的)的任务。
  2. 判断该任务的执行时间和当前时间的关系,如果执行时间小于等于当前时间 ,表示该任务满足执行条件,则将变变量taskFired置为true,同时重新计算该任务的下一次执行时间 ,并更新任务队列的顺序。
  3. 判断第2步中的taskFired,如果为false,表示执行时间还没到,所以调用queue.wait()方法等待,直到执行时间等于当前时间,等待完成之后,并不执行任务,而是进入下一轮循环,因为等待的时候可能有其他更紧急的任务插入到队列中,如果等待完直接执行任务,可能因为任务执行时间过长而耽误了新插入的更紧急任务。继续循环则可以立即发现最紧急的任务并执行。
  4. 如果第2步中的taskFired为true,则调用task.run()执行任务。需要注意的是,这里调用的是task.run(),并不启动新的线程,而是把task.run()当成普通方法调用。
  5. 如果任务队列为空,并且没有继续添加新任务的可能,就结束程序。

需要重点说明的是,第2步中,重新计算任务的下一次执行时间是如何进行的,其实代码很简单:

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);
        }

首先判断task.period是否为0,为0表示是一次性任务,该任务只执行一次,不会再次执行,所以将其从队列中删除,同时将task的状态标记为执行过。 如果不为0,表示为周期性任务,可以看到这里调用的queue.reshedualMin(time)方法,该方法的作用就是根据重新调整队列中任务的顺序,将最紧急的任务放在头部,以达到最紧急的任务最早执行的目的。

这里我们需要看一下下面一行代码

task.period < 0 ? currentTime - task.period : executionTime + task.period

这行代码求得的就是当前任务的下次执行时间。如果period小于0(为啥会小于0?在后面会有介绍),则表示该任务是通过schedule(TimerTask task, Date firstTime, long period) 方法提交的,即按照固定延迟来执行任务。 所以该任务的下次执行时间为currentTime - task.period,即此时此刻的时间加上延迟时间。

而如果period大于0,表示该任务是通过scheduleAtFixedRate(TimerTask task, Date firstTime, long period)方法提交的,即按照固定频率执行任务。所以该任务的下次执行时间为executionTime+task.period,即该任务的这次的执行时间加上延迟时间。

需要注意executionTime与currentTime的不同: schedule(TimerTask task, Date firstTime, long period) 方法,计算下一次执行时间是通过currentTime+period。当前时间为mainLoop方法运行到此时候的时间,是随着任务的执行长短变化的,比如任务开始执行时的时间(executionTime)为09:00:00,任务执行完成耗时10s,period为15s,然后程序在计算下次执行时间的时候,currentTime的值就是09:00:10(忽略其他耗时)。所以程序的下次执行时间就是09:00:25。 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)计算下一次执行时间是通过executionTime+periode。executionTime是本次任务开始执行的时间,这个是固定不变的,既成事实的。比如一个任务在09:00:00的是时候开始执行,它的peroid为15s,那么任务的下次执行时间就是09:00:15,再下次就是09:00:30,再下次就是09:00:45,但这个时间只是一个"理所应该的时间",就是说,按道理应该是分别在09:00:00,09:00:15,09:00:30,09:00:45的时候都执行一次,但是如果任务执行很耗时,假如任务执行需要30s(大于period15s),那么咋办呢?9:00:00程序开始执行,到了9:00:30的时候第一次任务才执行完,但是这时候15和30秒的时候的任务就被连累了,因为Timer是单线程 ,所以09:00:15的时候的任务并没有及时得到执行,但是当下一次循环的时候,最紧急的任务是09:00:15这个任务(假设期间没有更紧急的任务插入到队列中来),所以这时候程序会立即执行这个任务,而该任务的实际执行时间是09:00:30,而不是预计的09:00:15,所以,这个时间就是一个”理所应该的时间“。而这个任务执行完之后,又是30s,原本09:00:30和09:00:45该执行的任务都已经过期好久了,并没有按计划执行。

读完上面的文字你可能会问,为什么period会小于0呢?这个其实是Timer的一个实现机制,它是为了区分任务是按照固定延迟调度还是 固定频率,按照固定延迟的时候,它会将用户传过来的period变成负数,而按照固定频率则为大于0的数,只执行一次的,period为0。实际上从代码可读性的角度来讲,我觉得这样写并不好。代码如下:

//按照固定延迟
public void schedule(TimerTask task, Date firstTime, long period) {
     if (period <= 0)
          throw new IllegalArgumentException("Non-positive period.");
      sched(task, firstTime.getTime(), -period); //注意这里的-period
  }

可以看到,虽然其内部将period变成了负数,但是它们是不允许用户自己传递负数的。

//按照固定频率
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
  if (period <= 0)
      throw new IllegalArgumentException("Non-positive period.");
  sched(task, firstTime.getTime(), period);  //这里是正数
}

3.综上所述,总结一下:

  1. Timer 是一个单线程的,通过在Timer里面启动一个TimerThread线程来进行任务调度。TimerThread会一直循环,知道队列为空才停止。
  2. Timer 中维护者一个TaskQueue队列,该队列是一个按照任务执行时间从小到大排列的队列,每次插入或者删除元素的时候,都会进行一次重新排序,将时间最小的放到队列头部,以便于每次循环执行的都是最紧急的任务。TaskQueue中根据时间重新排序用到了堆排序算法。
  3. Timer提供了两个方法来支持周期性任务的执行,分别是按照固定时间间隔和按照固定频率执行。任务的实际执行时间都会受到任务执行时间长短的影响。当任务的执行时间小于调度方法参数中的period时,调度是没有问题的,当时间过长时,按照固定频率执行的任务将得不到有效控制。
  4. Timer中调度任务的时候用到了很多锁,同时还用到了queue的wait和notify方法,用的比较好,对于初学者来讲研究一下会很有收获。

调度任务的时候,如果当前任务的执行时间没到,那么会调用queue.wait()方法阻塞等待任务达到执行时间,这里为什么用queue.wait()呢?为什么不用task.wait()或者其他的wait?考虑下面一种情况, 如果当前任务A还需要20s才能开始执行 ,那么需要等待20s,但是在等待的时候,又有别的线程调用了timer的shedual方法,向Timer队列中添加了一个新的任务B,而B的执行时间为10s之后,显然,B的紧急程度要高于A的,所以这个时候应该先执行B任务,再执行A任务,所以在private void sched(TimerTask task, long time, long period)方法中可以看到如下代码:

synchronized(queue) {
           if (!thread.newTasksMayBeScheduled)
               throw new IllegalStateException("Timer already cancelled.");

           synchronized(task.lock) {
               if (task.state != TimerTask.VIRGIN)
                   throw new IllegalStateException(
                       "Task already scheduled or cancelled");
               task.nextExecutionTime = time;
               task.period = period;
               task.state = TimerTask.SCHEDULED;
           }

           queue.add(task);
           if (queue.getMin() == task)
               queue.notify();
       }

最后三行,可以看到,在向Timer中提交一个任务的时候,首先会将该任务提交到任务队列中,然后,从任务队列中获取一个时间最紧急的任务,如果这个时间最紧急的任务就是刚才提交到队列中的任务,那么调用queue.notify(),而该方法会中断queue.wait()方法,此时等待的任务A将不再等待,而是重新进行一次循环,从队列中重新取最紧急的任务去执行。

以下是mainLoop方法(省去大部分代码)

while (true) {
    try {
        TimerTask task;
        boolean taskFired;
        synchronized (queue) {
            //从队列中获取一个最紧急的任务
            //判断该任务的执行时间与当前时间的关系,如果《=,则taskFired为true,否则为false
            
            if (!taskFired) //任务没到执行时间,则等待,如果此时别的线程调用了queue.notify,该方法将被中断,然后进行下一次循环。
                queue.wait(executionTime - currentTime);
        }
        if (taskFired) // Task fired; run it, holding no locks
            task.run(); //如果满足执行条件,则执行任务
    } catch (InterruptedException e) {
    }
}
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
2年前
java目前可以通过以下几种方式进行定时任务
1、单机部署模式Timer:jdk中自带的一个定时调度类,可以简单的实现按某一频度进行任务执行。提供的功能比较单一,无法实现复杂的调度任务。ScheduledExecutorService:也是jdk自带的一个基于线程池设计的定时任务类。其每个调度任务都会分配到线程池中的一个线程执行,所以其任务是并发执行的,互不影响。
Easter79 Easter79
2年前
timer和ScheduledThreadPoolExecutor定时任务和每日固定时间执行
//ScheduledThreadPoolExecutor每三秒执行一次   publicstaticvoidmain(String\\args){      ScheduledThreadPoolExecutor schedulednewScheduledThreadPoolExecutor(2);      
Wesley13 Wesley13
2年前
Java实现几分钟之后调度任务的定时器
几分钟之后执行某一操作,使用定时器Timer可以实现,Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。具体实现如下:1packagecom.aone.foottalk.common;23importjava
Wesley13 Wesley13
2年前
@Scheduled注解
1概述@Scheduled注解是springboot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行.注意需要配合@EnableScheduling使用,配置@Scheduled主要有三种配置执行时间的方式,cron,fixedRate,fixedDelay.2croncron是
Wesley13 Wesley13
2年前
Java中使用Timer和TimerTask实现多线程
Timer是一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行,可以看成一个定时器,可以调度TimerTask。TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力。测试代码:import java.util.TimerTask;public class OneTas
Easter79 Easter79
2年前
SpringBoot定时任务动态修改cron表达式改变执行周期
一、场景引入前不久做过一个根据下载指令定时下载文件到服务器的需求。轮询下载的周期需要根据下载任务量的大小动态修改,下载任务密集的时候就周期缩小,下载任务少量时就扩大周期时间。java本身和第三方开源框架Spring共有三种执行定时任务的方式:1)Java自带的java.util.Timer类:这个类允许你调度一个java.util.TimerT
Easter79 Easter79
2年前
Springboot自带定时任务实现动态配置Cron参数
同学们,我今天分享一下SpringBoot动态配置Cron参数。场景是这样子的:后台管理界面对定时任务进行管理,可动态修改执行时间,然后保存入库,每次任务执行前从库里查询时间,以达到动态修改Cron参数的效果。好,咱们一起来看看是怎么回事。1.Timer:这是java自带的java.util.Timer类,这个类允许你调度一个j
Stella981 Stella981
2年前
Linux的定时任务
任务计划的条件:1.在未来的某个时间点执行一次某个任务(atbatch)2.周期性的执行某个任务(cron)at在指定时间执行任务_用法_at\选项参数\\时间\_选项参数_\l      查看作业\c      显示即将执行任务的细节\d      使用任务id号
Wesley13 Wesley13
2年前
Java线程之Timer
!在这里插入图片描述(https://oscimg.oschina.net/oscnet/730e89480439851f713afd6d740bc572b3c.jpg)简述java.util.Timer是一个定时器,用来调度线程在某个时间执行。在初始化Timer时,开启一个线程循环提取TaskQueue任务数组中的任务,如果任务数组为
Wesley13 Wesley13
2年前
Java 定时任务 & 任务调度
任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务。方式1:通过Thread来实现例如如下的代码,可以每隔1000毫秒做一次打印操作。publicclassJob_Schedule_Test1{publicstatic