定时任务-Timer和TimerTask

108 阅读8分钟

demo

以下是一个基本的计时器演示,它会在一定的时间间隔后执行指定的任务:

import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo {
    public static void main(String[] args) {
        //创建定时器
        Timer timer = new Timer();
        
        // 创建一个定时任务
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了!");
            }
        };
        
        // 在1000毫秒后开始执行任务,然后每2000毫秒执行一次
        timer.schedule(task, 1000, 2000);
    }
}

在这个例子中,我们使用了java.util.Timerjava.util.TimerTask类来创建一个简单的定时器演示。您可以根据需要调整定时器的延迟和执行间隔。在上述代码中,任务会在启动后1秒开始执行,然后每隔2秒执行一次。

实现原理

定时器的实现原理涉及Java中的 java.util.Timer 类和 java.util.TimerTask 类。以下是它们的基本工作原理:

  1. java.util.Timer 类: 这个类允许您创建定时任务,并安排它们在指定的时间点执行或以固定的时间间隔重复执行。它使用一个后台线程来调度任务的执行。

  2. java.util.TimerTask 类: 这是一个抽象类,表示一个要执行的定时任务。您需要扩展这个类并实现其 run() 方法,该方法包含实际任务的逻辑。

基本步骤是这样的:

  • 创建一个 Timer 实例:通过实例化 java.util.Timer 类来创建一个定时器对象。

  • 创建一个 TimerTask 实例:创建一个继承自 java.util.TimerTask 的类,并在其中实现 run() 方法,该方法定义了您希望在定时器触发时执行的任务逻辑。

  • 安排任务执行:使用定时器的 schedule()scheduleAtFixedRate() 方法,将创建的 TimerTask 对象与执行时间或执行间隔一起传递给定时器。这将安排定时器在指定的时间点执行任务,或者以指定的时间间隔重复执行任务。

  • 后台线程调度:定时器内部使用一个后台线程来调度任务的执行。它会在设定的时间点或时间间隔触发任务的 run() 方法。

  • 任务执行:当定时器触发时,在后台线程中执行与 TimerTask 关联的 run() 方法,从而执行您定义的任务逻辑。

需要注意的是,java.util.Timer 类在多线程环境中可能存在一些限制和性能问题。如果需要更高级的定时任务管理功能和更好的性能,您还可以考虑使用 java.util.concurrent.ScheduledExecutorService 接口提供的调度器。

这就是Java定时器的基本工作原理。通过使用这些类,您可以在指定的时间点执行任务,或者以固定的时间间隔重复执行任务,从而实现定时任务的管理和调度。

小结

其实就是三个核心类:

1、调度器Timer
添加自定义任务线程到任务队列

2、调度线程TimerThread
循环从任务队列出队任务执行

3、自定义任务线程TimerTask
自定义任务线程继承TimerTask

源码分析

入口

定时器Timer的入口:java.util.Timer#scheduleAtFixedRate(java.util.TimerTask, long, long)

/**
 * 添加业务线程到队列
 *
 * Schedules the specified task for repeated <i>fixed-rate execution</i>,
 * beginning after the specified delay.  Subsequent executions take place
 * at approximately regular intervals, separated by the specified period.
 *
 * <p>In fixed-rate execution, each execution is scheduled relative to the
 * scheduled execution time of the initial execution.  If an execution is
 * delayed for any reason (such as garbage collection or other background
 * activity), two or more executions will occur in rapid succession to
 * "catch up."  In the long run, the frequency of execution will be
 * exactly the reciprocal of the specified period (assuming the system
 * clock underlying <tt>Object.wait(long)</tt> is accurate).
 *
 * <p>Fixed-rate execution is appropriate for recurring activities that
 * are sensitive to <i>absolute</i> time, such as ringing a chime every
 * hour on the hour, or running scheduled maintenance every day at a
 * particular time.  It is also appropriate for recurring activities
 * where the total time to perform a fixed number of executions is
 * important, such as a countdown timer that ticks once every second for
 * ten seconds.  Finally, fixed-rate execution is appropriate for
 * scheduling multiple repeating timer tasks that must remain synchronized
 * with respect to one another.
 *
 * @param task   task to be scheduled. 业务线程
 * @param delay  delay in milliseconds before task is to be executed. 第一次执行的延迟时间
 * @param period time in milliseconds between successive task executions. 间隔时间
 * @throws IllegalArgumentException if {@code delay < 0}, or
 *         {@code delay + System.currentTimeMillis() < 0}, or
 *         {@code period <= 0}
 * @throws IllegalStateException if task was already scheduled or
 *         cancelled, timer was cancelled, or timer thread terminated.
 * @throws NullPointerException if {@code task} is null
 */
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");

    //添加业务线程到队列
    sched(task, System.currentTimeMillis()+delay, period);
}

添加业务线程到队列

/**
     * Schedule the specified timer task for execution at the specified
     * time with the specified period, in milliseconds.  If period is
     * positive, the task is scheduled for repeated execution; if period is
     * zero, the task is scheduled for one-time execution. Time is specified
     * in Date.getTime() format.  This method checks timer state, task state,
     * and initial execution time, but not period.
     *
     * @throws IllegalArgumentException if <tt>time</tt> is negative.
     * @throws IllegalStateException if task was already scheduled or
     *         cancelled, timer was cancelled, or timer thread terminated.
     * @throws NullPointerException if {@code task} is null
     */
    /**
     * 添加业务线程到队列
     *
     * @param task 业务线程
     * @param time 延迟时间:延迟多久开始执行——只在第一次执行的时候才延迟  //initialDelay – the time to delay first executio
     * @param period 间隔时间:每隔几秒,执行一次
     * @author javaself
     */
    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();
        }
    }

核心代码是这行

//先入队列
            queue.add(task);

作用是,先把当前业务线程添加到队列。

那业务线程什么时候被执行呢?下面会讲。

业务线程在哪里被执行?

业务线程是在调度线程里被执行的。

调度线程的入口:java.util.TimerThread#run

public void run() {
        try {
            //循环执行业务线程
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }

继续看循环方法:java.util.TimerThread#mainLoop

/**
 * 调度线程:循环执行业务线程。
 *
 * 主要步骤是
 * 1.从队列取业务线程
 * 2.然后,执行业务线程
 *
 * The main timer loop.  (See class comment.)
 */
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) {
        }
    }
}

主要步骤看代码注释,其实就是两步:

  1. 从队列里读数据
//获取时间最早的业务线程
task = queue.getMin();
  1. 然后,消费数据
if (taskFired)  // Task fired; run it, holding no locks
    //如果时间到了,就执行任务线程
    task.run(); //

这里的数据,指的就是业务线程。

读数据,就是指,从队列获取业务线程。

消费数据,就是指,执行业务线程。

而且,要注意,这个过程是循环的,即一直在循环处理,正常情况下,永不结束。


如何知道定时任务是否到期?

其实就是比较时间:比较到期时间和当前时间。

队列数据

队列数据

/**
 * Our Timer's queue.  We store this reference in preference to
 * a reference to the Timer so the reference graph remains acyclic.
 * Otherwise, the Timer would never be garbage-collected and this
 * thread would never go away.
 */
private TaskQueue queue;

TaskQueue源码

/**
 * 定时任务线程集合:准确的说是队列,而且是优先级队列。
 *
 * 基于二叉堆实现。
 *
 * This class represents a timer task queue: a priority queue of TimerTasks,
 * ordered on nextExecutionTime.  Each Timer object has one of these, which it
 * shares with its TimerThread.  Internally this class uses a heap, which
 * offers log(n) performance for the add, removeMin and rescheduleMin
 * operations, and constant time performance for the getMin operation.
 */
class TaskQueue {
    /**
     * Priority queue represented as a balanced binary heap: the two children
     * of queue[n] are queue[2*n] and queue[2*n+1].  The priority queue is
     * ordered on the nextExecutionTime field: The TimerTask with the lowest
     * nextExecutionTime is in queue[1] (assuming the queue is nonempty).  For
     * each node n in the heap, and each descendant of n, d,
     * n.nextExecutionTime <= d.nextExecutionTime.
     */
    private TimerTask[] queue = new TimerTask[128]; //优先级队列,基于二叉堆实现

其实是优先级队列


那怎么等待和唤醒?

其实就是queue.wait()/notify()

如果时间没到,就wait

如果时间到了,就notify,即执行任务

那调度线程本身在哪里被执行?

创建定时器的时候,就已经启动执行。后面如果有任务入队之后,就会一直循环从队列出队任务执行。

/**
 * Creates a new timer whose associated thread has the specified name.
 * The associated thread does <i>not</i>
 * {@linkplain Thread#setDaemon run as a daemon}.
 *
 * @param name the name of the associated thread
 * @throws NullPointerException if {@code name} is null
 * @since 1.5
 */
public Timer(String name) {
    thread.setName(name);

    //启动调度线程
    thread.start();
}

生产者消费者模式

定时器的主要步骤有两步

  1. 先把数据写到队列
  2. 然后,再消费数据

这两步是解耦的,是分开的,是独立的,不是按顺序执行的——说白了,就是典型的生产者和消费者模式。生产者和消费者,互不影响。