定时器 Timer

475 阅读8分钟

Timer 是一个调度任务的工具类,用于安排一个或多个定时任务在未来某个时间点或以固定的时间间隔执行。

Timer 是在 JDK 1.3 中引入的,包名为 java.util,它的引入使得开发者能够方便地安排和执行定时任务,满足基本的调度需求。

下面看看一下 Timer 调度任务的基本使用:

基本使用

定义任务

实现一个任务类,定义要执行的操作,例如检查超时订单并处理超时情况。

package com.cango.timer;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimerTask;

public class OrderTimeoutTask extends TimerTask {
    private  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public void run() {
        // 检查超时订单并执行处理
        checkAndHandleTimeoutOrders();
    }

    private void checkAndHandleTimeoutOrders() {
        // 实现检查和处理超时订单的逻辑
        LocalDateTime now = LocalDateTime.now();
        System.out.println("检查和处理超时订单:"+now.format(formatter));
    }
}

配置定时器

配置定时器,设定任务的执行频率和延迟。

package com.cango.timer;
import java.util.Timer;

public class TimerExample {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new OrderTimeoutTask(), 0, 10*1000); // 每10s执行一次
    }
}

输出结果如下:

检查和处理超时订单:2024-08-01 22:08:21
检查和处理超时订单:2024-08-01 22:08:31
检查和处理超时订单:2024-08-01 22:08:41
检查和处理超时订单:2024-08-01 22:08:51
检查和处理超时订单:2024-08-01 22:09:01
检查和处理超时订单:2024-08-01 22:09:11
检查和处理超时订单:2024-08-01 22:09:21
检查和处理超时订单:2024-08-01 22:09:31
检查和处理超时订单:2024-08-01 22:09:41
检查和处理超时订单:2024-08-01 22:09:51
检查和处理超时订单:2024-08-01 22:10:01

是不是非常简单。

Timer 相关类

image.png

TimerTask 定时任务

TimerTask 是一个抽象类,实现 Runnable接口,代表一个需要在未来某个时间点执行的任务。我们需要继承 TimerTask 类并重写其 run() 方法,来定义具体的任务逻辑。

TimerTask类的有几个属性需要了解一下:

Object lock:一个锁对象,用于控制对 TimerTask 内部状态的访问。

final Object lock = new Object();

一个 TimerTask 实例只有一个lock对象, TimerTask 内部的方法都用这个对象加锁。

那这个锁只能保证 TimerTask 自身成员属性的安全,用户继承其创建的自定义定时任务的线程安全怎么保证呢?

也就是说如果同一个实例被多个 Timer 执行,如果 run 方法会更改自定义定时任务的成员属性,这个线程安全怎么保证呢?

要用户自己保证喽。

int state:表示任务状态的整数。有四个可能的值:

VIRGINSCHEDULEDEXECUTEDCANCELLED
未调度,表示任务尚未安排执行已调度,表示任务已安排执行,如果它是非重复任务,则尚未执行已执行,表示非重复任务已经执行,尚未取消已取消,表示任务已经取消

long period : 表示重复任务的周期(以毫秒为单位)。

正数值(positive value)负数值(negative value)零值(0)
表示固定速率执行(fixed-rate execution)。这意味着任务将按照固定的间隔时间执行,无论任务执行所需的时间长短。表示固定延迟执行(fixed-delay execution)。这意味着任务将在上一次执行完成后,等待一个固定的时间间隔再执行,即使任务执行所需的时间较长。表示非重复任务(non-repeating task)。这意味着任务只执行一次,不会重复执行。

long nextExecutionTime:下一次执行任务的时间,以毫秒为单位。对于重复任务,在每次任务执行之前都会更新此字段。

Timer 怎么获取这个字段,执行该任务呢?

TaskQueue 任务队列

TaskQueueTimer 的内部类,它实现了一个优先队列(Priority Queue),用于管理定时任务(TimerTask)。

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

优先级由任务的 TimerTask 的成员属性 nextExecutionTime 字段决定,nextExecutionTime 越早的任务优先级越高。

说是队列并不准确,对其的增删改查方法都可以将队列调整为堆。

由此可知,一个 Timer 可以执行多个 TimerTask 任务。

TimerThread 定时器线程

TimerThread 也是 Timer 的内部类,它继承 Thread ,这个线程的主要职责是等待定时器队列中的任务,当任务触发时执行它们。

因为 Timer 内部只实例了一个线程,所以 Timer 是个单线程多任务的调度器。

这个类的核心就是一个无限循环方法 mainLoop ,它会不断从队列中获取任务并执行。

  1. 它首先检查优先队列是否为空,如果为空且可以调度新任务,则线程会等待 wait

  2. 如果队列不为空,它会获取优先队列中的第一个任务,也就是说 nextExecutionTime 最早的任务,并根据任务的执行时间来决定是否执行任务。(因为只有一个执行线程,如果多个任务具有相同的执行时间,那么它们的执行顺序可能无法预测。)

  3. 如果 nextExecutionTime 最早的任务被取消了,就将其从队列中清除;(如果任务在执行过程中被取消,它将继续运行,但不会再次运行。)

  4. 如果 nextExecutionTime 最早的任务,不到执行时间,就会等待 wait 指定时间。

  5. 如果 nextExecutionTime 最早的任务,到执行时间,就更改其状态为 EXECUTED

需要注意的是,上述步骤都在 synchronized(queue) 加锁中,而任务执行则在synchronized 加锁外,这样做的好处是避免死锁。

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; 
                // ... 获取符合执行条件的任务
             }
 
            if (taskFired)  // Task fired; run it, holding no locks
                task.run();
        } catch(InterruptedException e) {
            // `TimerTask`  异常不会影响其他任务的执行。
        }
    }
}

ThreadReaper 线程回收

ThreadReaper 也是 Timer 的内部类,实现 Runnable接口,表示它也是一个任务。

Timer 实例化的时候,这个任务会被注册进 Cleaner 里:

CleanerFactory.cleaner().register(this, threadReaper);

Cleaner 是 Java 提供的执行自定义清理操作的机制,它会在 Timer对象不再被引用时执行 ThreadReaper 回收任务。

Timercancel 方法也会手动结束线程:

public void cancel() {
    synchronized(queue) {
        queue.clear();
        cleanup.clean();
    }
}

当对象不再需要时,应该调用来终止后台线程,并清理资源。

ThreadReaper 执行逻辑代码很简单,仅仅是设置没有可调度的新任务,让队列不再接受新的任务,并唤醒线程,让线程自然执行,直至队列中没有可执行任务跳出无限循环:

public void run() {
    synchronized(queue) {
        thread.newTasksMayBeScheduled = false;
        queue.notify(); // In case queue is empty.
    }
}

Timer 定时器

了解了 TimerTaskTimer 内部类,再看 Timer 就很清楚了。

用户可以调用的 public 方法如下:

void schedule(TimerTask task, long delay):安排一个 TimerTask 在指定的延迟(delay)后执行一次。

void schedule(TimerTask task, Date time):安排 TimerTask 在指定的时间点(time)执行。

void schedule(TimerTask task, long delay, long period):安排一个 TimerTask 在固定延迟后开始,以给定的时间间隔(period)重复执行。

void schedule(TimerTask task, Date firstTime, long period):安排一个 TimerTask 从指定的时间点开始,以给定的时间间隔(period)重复执行。

void scheduleAtFixedRate(TimerTask task, long delay, long period):同 schedule 同参数方法,但 period 是负数,表示固定速率。

void scheduleAtFixedRate(TimerTask task, Date firstTime,long period):同 schedule 同参数方法,但 period 是负数,表示固定速率。

这些方法实现逻辑也很简单:

  1. 根据参数给 TimerTask 属性赋值,如 nextExecutionTimeperiodstate

  2. 将 TimerTask 添加进优先队列

  3. 如果添加的任务是第一个任务,就唤醒 TimerThread 起来执行任务

总结

Timer 大致实现方案如下:

任务队列Timer类内部维护了一个任务队列TaskQueue,这是一个优先队列,任务按其下一次执行时间排序。队列使用一个平衡的二叉堆实现,以提供高效的插入、删除和获取最小元素操作。

后台线程Timer类创建一个后台线程TimerThread,该线程从任务队列中取出任务并执行。如果任务是一次性的,执行后从队列中移除;如果是重复任务,则重新调度。

任务调度Timer类提供了多种方法来调度任务,包括一次性执行、按固定延迟执行和按固定速率执行。这些方法最终调用sched方法,该方法将任务添加到队列中,并根据任务类型设置其执行时间。

资源清理Timer类使用Cleaner来管理资源,确保当Timer对象不再被引用且队列中没有任务时,后台线程能够优雅地终止。

总而言之,Timer 因为是单线程执行任务,所以适用于一下简单的定时任务 。从 Java 5.0 开始,java.util.concurrent包引入了ScheduledThreadPoolExecutor,它是一个线程池,也可以以固定速率或延迟重复执行任务,是Timer的一个更优替代方案。