Java定时任务-Timer 1. Timer简单使用 2. Timer 3. 配置定时任务 4. 底层实现 5. Timer的不足

简单使用:

public class TimerTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建Timer对象
        Timer timer = new Timer();
        // 延迟1秒执行任务,只执行一次。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 1000);
        Thread.sleep(2000);
        timer.cancel(); // 取消任务执行,如果没有调用cancel,timer线程不会结束。
    }
}

TimerTask接口继承了Runnable,重写里面的run方法就可以自定义任务。

2. Timer

Timer类中,有两个重要字段:

/**
* The timer task queue.  This data structure is shared with the timer
* thread.  The timer produces tasks, via its various schedule calls,
* and the timer thread consumes, executing timer tasks as appropriate,
* and removing them from the queue when they're obsolete.
*/
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);

TaskQueue用来存储提交的定时任务的队列;TimerThread是Timer的内部类,继承了Thread类。

TaskQueue的底层实现是数组,它是一个优先队列。优先级参考下一次执行时间(每一个TimerTask下一次的执行时间),越快该执行的任务就会排的越靠前。

private TimerTask[] queue = new TimerTask[128];

TimerThread是一个线程,当Timer创建时,该线程就被启动:

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

3. 配置定时任务

主要用Timer中的schedule方法来配置不同的定时任务。

  • schedule(TimerTask task, long delay):在delay(毫秒)延迟后,执行任务task。只执行一次。
  • schedule(TimerTask task, Date time):在指定的时间点(time)执行task。如果时间点是过去某个时刻,那么该任务将被立即执行。
  • schedule(TimerTask task, long delay, long period):在当前时间delay(毫秒)延迟后执行task,然后每隔period(毫秒)执行一次
  • schedule(TimerTask task, Date firstTime, long period):在指定时间firstTime执行第一次task,然后每隔period(毫秒)执行一次。
  • scheduleAtFixedRate(TimerTask task, long delay, long period):在delay延迟后执行task,每隔period执行一次
  • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):在指定时间执行task,每隔period执行一次

3.1 schedule与scheduleAtFixedRate的区别

3.1.1 delay参数的方法

schedule(TimerTask task, long delay, long period)scheduleAtFixedRate(TimerTask task, long delay, long period) 的区别:
两者都是在当前时间的delay延迟后,执行第一次任务,并且之后每隔period执行一次。

区别在于:当任务的执行时间 > 任务间间隔时间(period)时,

  • schedule方法会阻塞,直到上一个任务执行完毕后,再执行下一个任务;
  • scheduleAtFixedRate方法不会阻塞,上一个任务即使未执行完毕,只要间隔时间足够period,就执行下一个任务。

在演示前,需要搞明白两个时间变量:

  • 系统时间(System.currentTimeMillis()或new Date()):正常流逝的时间。
  • 任务被执行的时间(TimerTask中的scheduledExecutionTime()方法):按照指定参数,任务应该在哪个时间点被执行。
    存在这一种情况:由于Timer是单线程执行任务,当任务的执行时间超过指定的任务间隔时间时,scheduleExecutionTime()方法的时间会早于系统时间

示例:schedule

public static void main(String[] args) throws InterruptedException {
    Timer timer = new Timer();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            // 我们在任务中分别打印“任务被执行时间”和当前系统时间
            System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
            System.out.println("new Date() : " + dateFormat.format(new Date()));
            try {
                // 阻塞6秒,超过了任务间隔4秒
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, 2000, 4000); // 指定任务执行间隔4秒
}

打印结果:

scheuledExecutionTime : 2020-08-23 13:49:33
new Date() : 2020-08-23 13:49:33
scheuledExecutionTime : 2020-08-23 13:49:39
new Date() : 2020-08-23 13:49:39
scheuledExecutionTime : 2020-08-23 13:49:45
new Date() : 2020-08-23 13:49:45
scheuledExecutionTime : 2020-08-23 13:49:51
new Date() : 2020-08-23 13:49:51
scheuledExecutionTime : 2020-08-23 13:49:57
new Date() : 2020-08-23 13:49:57

可以看到,任务执行时间(scheduleExecutionTime)和系统时间的间隔都是6秒。
任务执行的实际间隔是6秒,也就是上一个任务结束后,下一个任务才执行,并未按照设定的4秒间隔。 在schedule方法中,scheduleExecutionTime获取的是任务实际执行时的时间点。

示例:scheduleAtFixedRate

 public static void main(String[] args) throws InterruptedException {
     Timer timer = new Timer();
     SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
             System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
             System.out.println("new Date() : " + dateFormat.format(new Date()));
             try {
                 Thread.sleep(6000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }, 2000, 4000);
}

打印结果:

scheuledExecutionTime : 2020-08-23 14:31:54
new Date() : 2020-08-23 14:31:54
scheuledExecutionTime : 2020-08-23 14:31:58
new Date() : 2020-08-23 14:32:00
scheuledExecutionTime : 2020-08-23 14:32:02
new Date() : 2020-08-23 14:32:06
scheuledExecutionTime : 2020-08-23 14:32:06
new Date() : 2020-08-23 14:32:12
scheuledExecutionTime : 2020-08-23 14:32:10
new Date() : 2020-08-23 14:32:18

可以看到:任务被执行的时间(scheduleExecutionTime)的间隔是4秒,而打印的系统时间间隔是6秒。也就是说,任务的执行时间间隔按照设定的4秒进行,并未因上一个任务的阻塞而导致间隔延长。
但是注意:“scheuledExecutionTime”和“new Date() ”两句话都是同时打印的,都是在一个task开始时被打印。
在每一次任务执行时,下一次任务的scheuledExecutionTime()都会被计算好,因此,在scheduleAtFixedRate方法中,scheduleExecutionTime()中获取的是按照指定时间间隔任务应该被执行的时间点 ,但任务实际被执行的时间明显晚于该时间点。

3.1.2 firstTime参数的方法

schedule(TimerTask task, Date firstTime, long period)scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 的区别:
这两个方法的区别主要在于:
当firstTime早于当前系统时间时,执行的情况不同。

  • schedule:会立即执行task一次,之后每隔period执行一次;
  • scheduleAtFixedRate:会将从firstTime到当前系统时间(System.currentTimeMillis())之间应该执行的任务一次性执行完毕,然后继续按照period间隔执行。

示例:schedule

public static void main(String[] args) throws InterruptedException {
    Timer timer = new Timer();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 当前时间
    System.out.printf("=============当前时间: %s=================
", dateFormat.format(new Date()));
    // 从当前时间的20秒前开始执行任务
    Date startDate = Date.from(Instant.now().minusSeconds(20));
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
        }
    }, startDate, 4000); // 每隔4秒执行一次
}

打印结果:

=============当前时间: 2020-08-23 14:45:05=================
scheuledExecutionTime : 2020-08-23 14:45:05
scheuledExecutionTime : 2020-08-23 14:45:09
scheuledExecutionTime : 2020-08-23 14:45:13
scheuledExecutionTime : 2020-08-23 14:45:17
scheuledExecutionTime : 2020-08-23 14:45:21

可以看到,任务是从当前时间开始执行的,并未从startDate这个时间点开始。

示例:scheduleAtFixedRate

public static void main(String[] args) throws InterruptedException {
     Timer timer = new Timer();
     SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     // 当前时间
     System.out.printf("=============当前时间: %s=================
", dateFormat.format(new Date()));
     // 从当前时间的20秒前开始执行任务
     Date startDate = Date.from(Instant.now().minusSeconds(20));
     timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
             System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
         }
     }, startDate, 4000);
 }

打印结果:

=============当前时间: 2020-08-23 14:47:30=================
scheuledExecutionTime : 2020-08-23 14:47:10
scheuledExecutionTime : 2020-08-23 14:47:14
scheuledExecutionTime : 2020-08-23 14:47:18
scheuledExecutionTime : 2020-08-23 14:47:22
scheuledExecutionTime : 2020-08-23 14:47:26
scheuledExecutionTime : 2020-08-23 14:47:30
scheuledExecutionTime : 2020-08-23 14:47:34
scheuledExecutionTime : 2020-08-23 14:47:38
scheuledExecutionTime : 2020-08-23 14:47:42

可以看到:scheduleAtFixedRate方法将从开始时间(startDate 14:47:10)到当前时间之间(new Date() 14:47:30)之间的所有任务都执行一遍(这些任务都是同时执行,一次性输出的),然后从当前时间起继续以4秒为间隔往下执行。

4. 底层实现

每一个schedule方法的底层都是调用了sched方法。

private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");
    // Constrain value of period sufficiently to prevent numeric
    // overflow while still being effectively infinitely large.
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;
    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();
    }
}

该方法接收3个参数:

  • task:执行的任务;
  • time:任务下一次要执行的时间点;
  • period:执行任务的时间间隔。

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

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

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

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

5. Timer的不足

Timer是单线程的,不管任务多少,仅有一个工作线程;
限于单线程,如果第一个任务逻辑上死循环了,后续的任务一个都得不到执行。
依然是由于单线程,任一任务抛出异常后,整个 Timer 就会结束,后续任务全部都无法执行。

参考博客:https://www.cnblogs.com/yangming1996/p/10317949.html