Timer class principle analysis, task execution delay

Posted by mazen on Sun, 02 Jan 2022 11:06:57 +0100

API

The Timer class is used to delay the execution of a task.

  • schedule(TimerTask task, **long **delay) to specify the delay time
  • Schedule (TimerTask, date time), specify the time
  • schedule(TimerTask task, **long **delay, **long **period), specify the delay time and repetition period
  • Schedule (TimerTask task, date, firsttime, * * long * * period), specifying the initial time and repetition period

Source code interpretation

There are two main points to be curious about using API:

  1. How to execute regularly - how to ensure accurate scheduled execution? It seems almost impossible to complete
  2. For multiple tasks, how to execute subsequent tasks regularly if the current task is not completed.

Unfortunately, after reading the source code, I found that Timer did not give a good answer. It will roughly put the task into the priority queue, the execution time is key, and the earliest polling time will be executed. If the previous task takes too long, subsequent tasks will only be delayed later. Therefore, its solution is very simple. It does not guarantee the accurate time, nor does it guarantee that the task must be carried out according to the specified time.
Overall, it starts a thread subclass object that performs polling operations.

// Timer
public Timer(String name) {
    thread.setName(name);
    thread.start();
}
// TimerThread
public void run() {
    try {
        mainLoop();
    } finally {
        // Someone killed this Thread, behave as if Timer cancelled
        synchronized(queue) {
            newTasksMayBeScheduled = false;
            queue.clear();  // Eliminate obsolete references
        }
    }
}

The queue at the bottom of Timer is the smallest heap implemented by the array. Each time a task is added to the queue, the cycle period and the scheduling time nextExecutionTime will be set. To ensure thread safety, it locks the queue and the task itself separately.

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");
            // Set time
            task.nextExecutionTime = time;
            task.period = period;
            task.state = TimerTask.SCHEDULED;
        }

        queue.add(task);
        // If the currently added task is the oldest one, try to wake up the polling thread
        // Because during polling, the queue may be empty and waiting on the queue
        if (queue.getMin() == task)
            queue.notify();
    }
}

The core method of polling thread:

private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired;
            // Lock polling. If the queue is empty, wait
            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;
                // Get the task that needs to be performed first
                task = queue.getMin();
                synchronized(task.lock) {
                    if (task.state == TimerTask.CANCELLED) {
                        queue.removeMin();
                        continue;  // No action required, poll queue again
                    }
                    // Absolute time as reference
                    currentTime = System.currentTimeMillis();
                    executionTime = task.nextExecutionTime;
                    // If the predetermined time is less than the current time, the flag is executable
                    if (taskFired = (executionTime<=currentTime)) {
                        // If periodic execution is not required, remove. Otherwise, reset the next time
                        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);
            }
            // implement
            if (taskFired)  // Task fired; run it, holding no locks
                task.run();
        } catch(InterruptedException e) {
        }
    }
}

shortcoming

After reading the source code, we can understand some of its shortcomings:

1. Firstly, Timer's support for scheduling is based on absolute time rather than relative time, so it is very sensitive to the change of system time.

2. Secondly, the Timer thread will not catch exceptions. If an unchecked exception is thrown by the TimerTask, the Timer thread will terminate. At the same time, the Timer will not resume the execution of the thread. He will mistakenly think that the whole Timer thread will be cancelled. At the same time, TimerTask that has been scheduled but not yet executed will not be executed, and new tasks cannot be scheduled. Therefore, if TimerTask throws an unchecked exception, Timer will produce unpredictable behavior

3. Timer will only create one thread task when executing a scheduled task. If there are multiple threads, if the execution time of a thread task is too long for some reason, exceeding the interval between the two tasks, the execution time of the next task will lag
--------
From Java Concurrent Programming Practice

Topics: Java Back-end Concurrent Programming