一、定时任务

在 java.util 包下提供了 Timer 与 TimerTask 用于提交定时任务,其中 Timer 用于管理定时任务,TimerTask 创建定义定时任务的具体执行内容。

1. 定时任务

TimerTask 使用类似于线程,新建任务类 CustomTask 继承 TimerTask 类并重写 run() 方法,在 run() 方法中定义任务的具体执行逻辑。

如下示例中我定义了一个定时任务打印当前系统时间并自增计数器 num

class CustomTask extends TimerTask {

    private String taskName;

    public CustomTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
            String nowadays = formatter.format(LocalDateTime.now());
            System.out.printf("Schedule [%s] active, current time: %s%n", this.taskName, nowadays);
        } catch (Exception ex) {
            System.out.println("Error running thread " + ex.getMessage());
        }
        num.getAndIncrement();
    }
}

2. 任务提交

完成定时任务的创建之后即可通过 Timer 类的 schedule() 方法进行提交,Timer 中维护了一个默认大小为 128 的工作队列用于接收传入定时任务。

schedule(task, delay, period) 方法的三个入参描述参考下表。

方法 描述
task 需要指定的定时任务。
delay long 类型,任务首次触发时间与当前的时间差。
period long 类型,定时任务间隔周期。

如下示例中通过 Timer 提交了一个定时任务,并设置在 2 秒后触发,任务每隔 5 秒执行一次。

public class ScheduleTest {

    private volatile AtomicInteger num = new AtomicInteger(0);

    @Test
    public void timeTaskDemo() throws InterruptedException {
        Timer time = new Timer();
        long delay = TimeUnit.SECONDS.toMillis(2);
        long period = TimeUnit.SECONDS.toMillis(5);
        time.schedule(new CustomTask(), delay, period);

        while (true) {
            if (num.get() > 3) {
                time.cancel();
                System.out.println("Cancel time task.");
                break;
            }
        }
        // purge(): Remove the cancelled task from time queue.
        // If timer queue is empty that is eligible for gc
        time.purge();
    }
}

除了 schedule() 方法 Timer 类中还提供其它相应的操作,具体信息参考下表:

方法 描述
cancel() 清空排队中定时任务并不再接收新提交任务,但是正在执行的任务不会中断。
purge() 清空队列中已经被 cancel 的任务,当队列为空时即可被 GC。

二、延时类

1. 基本介绍

在 Java 中提供了 Delayed 接口类以供延迟队列,延迟队列中的元素必须实现 Delayed 接口。

Delayed 接口中包含了重要接口方法 getDelay(),新建自定义延迟任务类 DelayTask 并重新该方法,当延迟队列 DelayQueue 在获取元素即会调用该方法。

class DelayTask<T> implements Delayed {

    private final long EXPIRE = TimeUnit.SECONDS.toMillis(10);

    private T t;
    private long startTime;

    public DelayTask(T t, long startTime) {
        this.t = t;
        this.startTime = startTime + EXPIRE;
    }

    /**
     * Get element will activate getDelay().
     * <p>
     * If "interval" lower the zero then retrieved to get.
     */
    @Override
    public long getDelay(TimeUnit unit) {
        long interval = startTime - System.currentTimeMillis();
        // convert time interval to milliseconds
        return unit.convert(interval, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        DelayTask<T> task = (DelayTask<T>) o;
        return Ints.saturatedCast(startTime - task.startTime);
    }
}

2. 任务提交

完成延迟任务对象定义之后即可创建延迟队列,其常用方法参考下表:

方法 作用
add() 添加延时任务至队列,返回 boolean 值。
put() 添加延时任务至队列,无返回值。
take() 以阻塞的方式获取队列元素。

这里重点介绍一下 take() 方法的作用效果。

当 take() 获取队列元素时将会触发 Delayed 接口的 getDelay() 方法。

  • 若返回值小于等于 0,返回队列元素。
  • 若返回值大于 0,则访问队头的后一位元素。
  • 若队列中所有元素的 getDelay() 返回值皆大于 0take() 将一直处于阻塞状态。
@Test
public void delayDemo() throws InterruptedException {
    DelayQueue<DelayTask<String>> delayQueue = new DelayQueue<>();
    delayQueue.put(new DelayTask<>("Alex", System.currentTimeMillis()));
    TimeUnit.MILLISECONDS.sleep(1000);
    delayQueue.put(new DelayTask<>("Beth", System.currentTimeMillis()));

    /*
     * task(): Get queue head element and active getDelay() result
     * ==> 1. result <= 0: return element.
     * ==> 2. result > 0: skit element and visit next, if all > 0 then block.
     */
    DelayTask<String> task = delayQueue.take();
    System.out.println("Task: " + task);
}

三、定时线程池

1. 基本介绍

上述提到 Timer 类中维护了一个工作队列并以单线程执行定时任务,而 ScheduledExecutorService 可用于创建定时线程任务资源池。

ScheduledExecutorService 提交任务不再限制必须继承于 TimerTask,任务类继承于线程同样允许。

2. 任务提交

通过 newScheduledThreadPool() 创建定时任务线程池资源,通过构造器指定线程数。

方法 作用
schedule() 提交定时任务,仅执行一次。
scheduleAtFixedRate() 提交定时任务,任务触发间隔周期是按上次任务开始时间计算。
scheduleWithFixedDelay() 提交定时任务,任务触发间隔周期是按上次任务完成时间计算。
public void scheduledPoolDemo() throws Exception {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
    int start = 1;
    int interval = 3;
    // 当前时间 1 秒后执行一次
    executor.schedule(() -> {
        System.out.println("Task-1 running.");
    }, interval, TimeUnit.SECONDS);
    
    // 当前时间 1 秒后执行, 且每隔 3 秒重复执行
    // (间隔时间:上一次任务开始时计时)
    executor.scheduleAtFixedRate(new Task("fixed-rate"), start, interval, TimeUnit.SECONDS);

    // 当前时间 1 秒后执行, 上一任务执行完成 3 秒后重复执行
    // (间隔时间:上一次任务结束时计时)
    executor.scheduleWithFixedDelay(new Task("fixed-delay"), start, interval, TimeUnit.SECONDS);
    executor.shutdown();
}

需要注意 scheduleAtFixedRate 与 scheduleWithFixedDelay 的区别。

  • scheduleAtFixedRate()间隔时间是从上一个任务开始时计算,无论上个任务是否已经结束。
  • scheduleWithFixedDelay()间隔时间是从上个任务完成时开始计算,只有当上个任务结束才会开始计时。
scheduleAtFixedRate 与 scheduleWithFixedDelay