JDK source reading: java timing tasks, Timer and Schedule ThreadPool Executor

Keywords: Java Spring JDK less

Article directory


Task scheduling refers to the automatic execution of tasks based on a given time point, a given time interval or a given number of executions.

Four Java implementations of task scheduling:

  • Timer
  • ScheduledExecutor
  • Timing tasks provided by spring
  • Open Source Toolkit Quartz

The implementation of timing tasks provided by Open Source Toolkit and Spring is introduced in the following chapters. This paper mainly introduces the implementation of timing tasks provided by JDK. In addition, in order to achieve complex task scheduling, this paper also introduces some methods of using Calendar.

Timer

java.util.Timer is the simplest way to achieve task scheduling. Here is a concrete example.

package huyp.task.timer;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Task Implementation Class
 * */
public class TimerTest extends TimerTask{

  private String jobName="";
  
  private static String format = "yyyy-MM-dd HH:mm:ss";
  static DateFormat dateFormat = new SimpleDateFormat(format);
  
  public TimerTest(String jobName) {
    super();
    this.jobName=jobName;
  }
  
  @Override
  public void run() {
    System.out.println("now time"+dateFormat.format(new Date())+" execute "+jobName);
    System.out.println("Current thread"+Thread.currentThread().getName());
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    Timer timer=new Timer();
    long delay1=1*1000;
    long period1=1000;
    System.out.println("now time"+dateFormat.format(new Date()));
    //After 1 s from now on, job1 is executed every 1 s
    timer.schedule(new TimerTest("job1"), delay1,period1);
    
    long delay2=2*1000;
    long period2=2000;
    //After 2 seconds from now on, job2 is executed every 2 seconds
    System.out.println("now time"+dateFormat.format(new Date()));
    timer.schedule(new Timer2Test("job2"), delay2,period2);
    
  }
}
package huyp.task.timer;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimerTask;

public class Timer2Test extends TimerTask{
  private String jobName = "";

  private String format = "yyyy-MM-dd HH:mm:ss";
  DateFormat dateFormat = new SimpleDateFormat(format);
  
  public Timer2Test(String jobName) {
    super();
    this.jobName=jobName;
  }

  @Override
  public void run() {
    System.out.println("now time"+dateFormat.format(new Date())+" execute "+jobName);
    System.out.println("Current thread"+Thread.currentThread().getName());
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Operation results:

Design core

The core classes of task scheduling using Timer are Timer and TimerTask. Timer is responsible for setting the start and interval execution time of TimerTask. Users only need to create an inheritance class of TimerTask, implement their own run method, and then throw it to Timer to execute. TimerTask is a class that implements Runnable, and the run() method is the specific task content.

There is a task queue and a task thread in the Timer class

private final TaskQueue queue = new TaskQueue();

private final TimerThread thread = new TimerThread(queue);

TaskQueue task queue is actually an array of TimerTask s

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

Constructor

public Timer() {
    this("Timer-" + serialNumber());
}

public Timer(boolean isDaemon) {
    this("Timer-" + serialNumber(), isDaemon);
}

public Timer(String name) {
    thread.setName(name);
    thread.start();
}

public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

As you can see from the source code, when you create a Timer, you actually start the TaskThread thread and become a daemon.

//TaskThread implements the run method, which is called when a thread calls the start method
public void run() {
    try {
        mainLoop();
    } finally {
        // Someone killed this Thread, behave as if Timer cancelled
        synchronized(queue) {
            newTasksMayBeScheduled = false;
            queue.clear();  // Eliminate obsolete references
        }
    }
}

//
private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired;
            synchronized(queue) {
                // Wait for queue to become non-empty
                while (queue.isEmpty() && newTasksMayBeScheduled)
                    //When the task queue is empty, it is blocked, waiting for notify to be aroused
                    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();
                //Get the task object in the data and lock it
                synchronized(task.lock) {
                    if (task.state == TimerTask.CANCELLED) {
                        //If the task has been cancelled, it is removed from the queue
                        queue.removeMin();
                        continue;  // No action required, poll queue again
                    }
                    currentTime = System.currentTimeMillis();
                    executionTime = task.nextExecutionTime;
                    //Determine whether the task is outdated or not, and if it is not, it can be triggered
                    if (taskFired = (executionTime<=currentTime)) {
                        //Determine whether to repeat, if not, delete from the queue, and modify the task status
                        if (task.period == 0) { // Non-repeating, remove
                            queue.removeMin();
                            task.state = TimerTask.EXECUTED;
                        } else { // Repeating task, reschedule
                            //Recalculate the next execution time, and then sort by execution time
                            queue.rescheduleMin(
                                task.period<0 ? currentTime   - task.period
                                : executionTime + task.period);
                        }
                    }
                }
                //If the task has not been triggered, wait for a certain time, in order to avoid only one task in the queue because the departure time is less than a dead cycle, so wait, if other tasks are added to the task queue at this time, it will not wait.
                if (!taskFired) // Task hasn't yet fired; wait
                    queue.wait(executionTime - currentTime);
            }
            //If the task has been triggered, call the run method of TimerTask to run the task
            if (taskFired)  // Task fired; run it, holding no locks
                task.run();
        } catch(InterruptedException e) {
        }
    }
}

schedule

//delay Delayed Time Delayed Tasks
public void schedule(TimerTask task, long delay) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    sched(task, System.currentTimeMillis()+delay, 0);
}

//Timing task
public void schedule(TimerTask task, Date time) {
    sched(task, time.getTime(), 0);
}

//Delayed periodic tasks
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);
}

//Timed periodic tasks
public void schedule(TimerTask task, Date firstTime, long period) {
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), -period);
}

//Time: Execution time: Interval time
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");
                //Setting the execution time, interval, and status of tasks
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }

            //Add tasks to the queue and sort them according to their next execution time
            queue.add(task);
            if (queue.getMin() == task)
                //When the task is added to the queue, the queue is not empty and the execution time is the most advanced, the TaskThread daemon thread is called from the queue.wait method.
                queue.notify();
        }
    }

Advantages and disadvantages

Timer has the advantage of simplicity and ease of use, but because all tasks are scheduled by the same thread, all tasks are executed serially, and only one task can be executed at the same time. The delay or exception of the former task will affect the later task.

Operating trajectory:

ScheduledThreadPoolExecutor

Given Timer's above shortcomings, Java Scheduled ThreadPool Executor based on thread pool design was introduced. Its design idea is that every task scheduled will be executed by a thread in the thread pool, so if it is executed by multi-threads, the tasks will be executed concurrently and will not be interfered with each other. It should be noted that only when the execution time of the task arrives can a thread be started, and the rest of the time is polling the status of the task; if the task is executed using a single thread pool, the task is executed serially.

From the previous chapter, you can see that the underlying implementation is consistent with the common thread pool, as long as the delayed queue DelayWorkQueue is used.

DelayWorkQueue

DelayWorkQueue is implemented based on blocking queue DelayQueue

static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {

        private static final int INITIAL_CAPACITY = 16;
        private RunnableScheduledFuture<?>[] queue =
            new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
        private final ReentrantLock lock = new ReentrantLock();
        private int size = 0;
        
        private final Condition available = lock.newCondition();

        }

ScheduleExecutorService

The above figure is the inheritance structure of Java thread pool. Executor Service mainly defines the general method of thread pool. Schedule Executor Service defines four method definitions of timing task.

schedule

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    //Wrap the task as Runnable Scheduled Future
    //triggerTIme is used to calculate trigger time
    RunnableScheduledFuture<?> t = decorateTask(command,
                                                new ScheduledFutureTask<Void>(command, null,
                                                                              triggerTime(delay, unit)));
    //Deferred execution
    delayedExecute(t);
    return t;
}

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
        reject(task);
    else {
        //Place tasks in a waiting queue
        super.getQueue().add(task);
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
            ensurePrestart();
    }
}

void ensurePrestart() {
    //Determine the number of threads executing in the thread pool, and then call the addWorker method
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

You've learned from the thread pool in the previous chapter that addWorker() encapsulates the task as a Worker object, then calls the start() method of the Worker object to execute the run() method of the task, which executes the run method of the Scheduled FutureTask object.

public void run() {
    //Is the task performed at intervals?
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    //If the task does not need to be repeated, call the run method of the parent class
    else if (!periodic)
        ScheduledFutureTask.super.run();
    //If the task needs to be executed at intervals, run and reset the task, then calculate the next run time
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

scheduleAtFixedRate

Each execution time is an interval between the start of the last task and the start of the last task.

scheduleWithFixedDelay

Each execution time is a time interval pushed back from the end of the last task

Complex Task Scheduling

Both Timer and Scheduled ThreadPool Executor can only provide task scheduling based on start time and repetition interval, and are not competent for more complex scheduling requirements. For example, set 16:38:10 every Tuesday to perform tasks. This function can not be implemented directly using Timer and Scheduled Executor, but we can implement it indirectly with Calendar.

Its core is to calculate the absolute time of the last Tuesday at 16:38:10 based on the current time, and then calculate the time difference with the current time as a parameter to call the Scheduled Exceutor function.

Computing the execution time of complex task scheduling by Calendar is very complex, which requires a more perfect task scheduling framework to solve these complex scheduling problems. The open source framework Quartz will be introduced in the following chapters.

Posted by Haroskyline on Mon, 06 May 2019 02:50:39 -0700