Java定时任务之Timer

410 阅读3分钟

在JDK1.3中,新增了java.util.Timer来实现定时功能。它简单易用,但不足也非常明显。

  • Timer内只有一个线程,因此所有任务都是串行执行的;
  • 如果Timer中线程因异常退出,将不会创建新线程来继续执行任务;
  • 前一个任务的延迟或异常,都将影响到之后的任务执行。

生产中,我们几乎不会使用Timer来做定时任务,更多使用定时线程池ScheduledExecutorService,框架如Quartz、xxl-job、elastic-job等。本文将为你学习它们打个基础。

1 定时器Timer

它有两个主要属性:任务队列TaskQueue和工作线程TimerThreadimage.png 创建Timer时:

  1. 默认名称为"Timer-" + serialNumber
  2. 可以设置Thread为守护线程;
  3. 在调用new Timer()时,将启动任务线程。 image.png

向Timer提交任务的方法有:

// 提交仅执行一次的延迟任务
public void schedule(TimerTask task, long delay)
// 提交周期任务
public void scheduleAtFixedRate(TimerTask task, long delay, long period)

2 TimerThread

任务执行的工作线程,它从任务队列获取任务,在它们到期时执行它们。

  • 仅执行一次的delay任务,到期执行后从队列中移除;
  • 周期执行的定时任务,本次执行后将重新计算下次执行时间,再放回到队列中。

它有两个属性:

  1. newTasksMayBeScheduled,标识线程是否继续工作,默认为true;
  2. queue,任务队列。 image.png

run方法调用了mainLoop(),死循环来持续处理任务。 image.png

2.1 周期任务如何执行?

mainLoop()中, 主要逻辑如下: image.png

  • 每次循环,只从队列中获取到期时间最早的一个任务;
  • 根据task.executionTime<=currentTime,判断任务到期否;
  • delay任务(task.period == 0),仅执行一次就从队列中移除;
  • 周期任务在本次执行前,会更新task.executionTime为下次执行时间,并在队列中重排序; image.png
  • task.run是在工作线程中执行的;即使有多个任务同时到期,也得串行依次执行;

2.2 工作线程何时退出呢?

从源码中可以看到有4种情况:

  1. 任务抛出InterruptedException之外的异常,不会被catch,进而结束。
private void mainLoop () {
  while (true) {
    try {
      // 执行任务
    } catch (InterruptedException e) {
    }
  }
}
  1. newTasksMayBeScheduled=false时,不会调用queue.wait(),如果队列为空则break跳出死循环而结束; image.png
  2. 主动调用Timer#cancel方法,清空队列,终止定时器。 image.png
  3. new Timer()时设置工作线程为daemon的,主线程终止时Timer也退出。

3 TimerTask

TimerTask是Runnable接口的抽象子类,很简单,仅有如下属性:

// synchronized锁对象,更新task属性时需加锁
final Object lock = new Object();
// 状态,默认为VIRGIN即尚未被调度;
// 可取值还有SCHEDULED、EXECUTED、CANCELLED
int state = VIRGIN;
// 到期时间,毫秒值
long nextExecutionTime;
// 执行周期
long period = 0;

对task属性的修改,使用synchronized(task.lock)来防止并发。

4 TaskQueue

一个定时器任务的优先级队列,根据TimerTask的nextExecutionTime排序。在内部使用一个堆,它为add、removeMin和rescheduleMin操作提供log(n)性能,为getMin操作提供常数时间性能。

基于数组实现,初始大小为128,size到达上限时,扩容到两倍大小。 image.png image.png 对queue的操作,使用synchronized(queue)防止并发。

5 使用Timer时的注意事项

5.1 串行特征

下面代码中,task1和task2都延迟1秒后执行。由于task1耗时2秒,导致task2在第3秒时才被执行。

import lombok.extern.slf4j.Slf4j;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestTimer")
public class TestTimer {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
    Timer timer = new Timer();
    TimerTask task1 = new TimerTask() {
      @Override
      public void run() {
        log.debug("task 1");
        sleep(2);
      }
    };

    TimerTask task2 = new TimerTask() {
      @Override
      public void run() {
        log.debug("task 2");
      }
    };

    log.debug("start...");
    timer.schedule(task1, 1000);
    timer.schedule(task2, 1000);
  }
}

image.png

5.2 异常退出

还是上面的代码,只是在task1中抛出异常。结果TimerThread退出,不会执行task2。

    Timer timer = new Timer();
    TimerTask task1 = new TimerTask() {
      @Override
      public void run() {
        log.debug("task 1");
        // 模拟业务异常
        int i = 1 / 0;
      }
    };

    TimerTask task2 = new TimerTask() {
      @Override
      public void run() {
        log.debug("task 2");
      }
    };

    log.debug("start...");
    timer.schedule(task1, 1000);
    timer.schedule(task2, 1000);

image.png