Java timed task - Timer principle

Posted by CGRRay on Thu, 27 Jan 2022 19:10:56 +0100

Java timed task - Timer principle

outline

The Jdk library comes with two technologies to realize timed tasks. One is through Timer, and the other is through ScheduledThreadPoolExecutor. The following is an analysis of the principle of Timer implementation.

1, Timer

1. Timer usage

public class TimerTest extends TimerTask {

    @Override
    public void run() {
        System.out.println("test1 --------- " + Thread.currentThread());
    }

    static class TimerDemo {
        public static void main(String[] args) {
            Timer timer = new Timer();
            // The run method in TimerTest is executed after three seconds and every 1 second
            timer.schedule(new TimerTest(), 3000, 1000);
        }
    }
}

2. Source code analysis

After seeing the above practice, do you think it's very simple? Then we analyze the above code.

First, notice that the TimerTest class inherits TimerTask

TimerTask
public abstract class TimerTask implements Runnable {
    /**
     * This object is used to control access to the TimerTask internals.
     */
    final Object lock = new Object();

    /**
     * The state of this task, chosen from the constants below.
     */
    int state = VIRGIN;

    /**
     * This task has not yet been scheduled.
     */
    static final int VIRGIN = 0;

    /**
     * This task is scheduled for execution.  If it is a non-repeating task,
     * it has not yet been executed.
     */
    static final int SCHEDULED   = 1;

    /**
     * This non-repeating task has already executed (or is currently
     * executing) and has not been cancelled.
     */
    static final int EXECUTED    = 2;

    /**
     * This task has been cancelled (with a call to TimerTask.cancel).
     */
    static final int CANCELLED   = 3;

    /**
     * Next execution time for this task in the format returned by
     * System.currentTimeMillis, assuming this task is scheduled for execution.
     * For repeating tasks, this field is updated prior to each task execution.
     */
    long nextExecutionTime;

    /**
     * Period in milliseconds for repeating tasks.  A positive value indicates
     * fixed-rate execution.  A negative value indicates fixed-delay execution.
     * A value of 0 indicates a non-repeating task.
     */
    long period = 0;

    /**
     * Creates a new timer task.
     */
    protected TimerTask() {
    }

    /**
     * The action to be performed by this timer task.
     */
    public abstract void run();

    public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }

    public long scheduledExecutionTime() {
        synchronized(lock) {
            return (period < 0 ? nextExecutionTime + period
                               : nextExecutionTime - period);
        }
    }
}

After removing the comments, the code of this class will be dozens of lines. You can see that TimerTask implements the Runnable interface and has an abstract run() method. Our TimerTest inherits and implements the run() method. And this class has several member variables:

Lock: used to lock threads

state: indicates the status of the task

nextExecutionTime: the time of the next task execution

Period: the period during which the task is executed

There are only two ways:

cancel(): cancels the task and changes the state of the task

scheduledExecutionTime(): sets the next execution time

Timer

Let's analyze the Timer class. First attach a mind map to facilitate memory and understanding

The following is a brief list of the key codes of Timer. You can check the specific codes yourself

public class Timer {
    /** It is used to store the tasks to be executed and maintain the queue through the minimum heap**/
    private final TaskQueue queue = new TaskQueue();
    
    /** The thread responsible for executing the task**/
    private final TimerThread thread = new TimerThread(queue);
    
    /** The constructor sets whether the thread name is a daemon and finally starts the thread**/
    public Timer(String name, boolean isDaemon) {
        thread.setName(name);
        thread.setDaemon(isDaemon);
        thread.start();
    }
}

Let's take a look at the TimerThread class

TimerThread
class TimerThread extends Thread {
    
    boolean newTasksMayBeScheduled = true;
    
    private TaskQueue queue;
    
    public void run() {
        try {
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }
    
    /** Key code execution scheduled tasks**/
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // If the queue is empty and newtasks maybescheduled is true, the thread enters the waiting state
                    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;
                    // Gets the earliest task to be executed in the queue
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            // If the status is cancelled, the task will be deleted from the queue
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        // Get current time
                        currentTime = System.currentTimeMillis();
                        // Gets the time when the next task is to be executed
                        executionTime = task.nextExecutionTime;
                        // If the task execution time is less than or equal to the current time, it will start execution
                        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);
                            }
                        }
                    }
                    // Task hasn't yet fired; wait
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                //If the execution time of the task is up, execute the task
                if (taskFired)  
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
}

We have a general idea of how TimerThread circularly executes scheduled tasks. Next, let's take a look at Timer's schedule() and sched() methods

schedule & sched
public void schedule(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);
}


private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");

    // Get the absolute value of the execution cycle period, if it is greater than half of long MAX_ Value, then divide the period by two
    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;
        }

        // Put in queue
        queue.add(task);
        // If the task is added to the queue and left at the top of the heap, the thread will be directly awakened to execute the task (remember to let the thread wait in TimerThread before?)
        if (queue.getMin() == task)
            queue.notify();
    }
}

3. Summary

When we create Timer, we will initialize and start a thread to execute tasks and initialize a priority queue. We use the minimum heap data structure to put the earliest executed task on the top of the heap.

When we call the schedule() method, we put the task in the priority queue. The TimerThread loop judges whether the execution time has been reached. If so, first calculate the next execution time and adjust the heap. Finally, execute the task. If not, the thread enters the wait.

Topics: Java