ScheduledThreadPoolExecutor parsing
We know that Timer and TimerTask can realize the periodic and delayed scheduling of threads, but Timer and TimerTask have some defects. Therefore, we generally recommend ScheduledThreadPoolExecutor to implement this scheduling strategy of regularly and periodically executing tasks. The following is an in-depth analysis of how the ScheduledThreadPoolExecutor implements thread cycle and delay scheduling.
ScheduledThreadPoolExecutor inherits ThreadPoolExecutor and implements ScheduledExecutorService interface, which is equivalent to ThreadPoolExecutor providing "delay" and "periodic execution" functions. It is defined in the JDK API as ThreadPoolExecutor, which can be scheduled to run commands after a given delay or execute commands regularly. This class is superior to Timer when multiple worker threads are required, or when ThreadPoolExecutor is required to have additional flexibility or functionality. Once a deferred task is enabled, it is executed, but there is no real-time guarantee about when it is enabled and when it will be executed after it is enabled. Tasks scheduled to be executed at the same time are enabled according to the submitted first in first out (FIFO) order.
It provides four construction methods:
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory); } public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler); } public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler); }
Of course, we generally do not generate a ScheduledThreadPoolExecutor object directly through its constructor (such as new ScheduledThreadPoolExecutor(10)), but through the Executors class (such as Executors.newScheduledThreadPool(int);)
In the constructor of ScheduledThreadPoolExecutor, we found that it is constructed using ThreadLocalExecutor. The only change is that the blocking queue it uses becomes DelayedWorkQueue instead of the LinkedBlockingQueue of ThreadLocalhExecutor (ThreadLocalhExecutor object is generated through Executors). DelayedWorkQueue is an internal class in ScheduledThreadPoolExecutor, which is actually a bit similar to the blocking queue DelayQueue. DelayQueue is a blocking queue that can provide delay. It can extract elements from it only when the delay expires. Its column header is the Delayed element with the longest storage time after the delay expires. If none of the delays have expired, the queue has no header and poll will return null. Therefore, the tasks in the DelayedWorkQueue must be sorted according to the delay time from short to long. Let's go into the DelayedWorkQueue and leave an introduction here.
ScheduledThreadPoolExecutor provides the following four methods, namely four schedulers:
- Schedule (callable, callable, long delay, timeunit unit): create and execute the ScheduledFuture enabled after a given delay.
- schedule(Runnable command, long delay, TimeUnit unit): create and execute a one-time operation enabled after a given delay.
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): create and execute a periodic operation that is enabled for the first time after a given initial delay, and subsequent operations have a given period; That is, it will be executed after initialDelay, then after initialDelay+period, then after initialDelay + 2 * period, and so on.
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): create and execute a periodic operation that is enabled for the first time after a given initial delay. Then, there is a given delay between the termination of each execution and the start of the next execution.
The first and second methods are almost one-time operations, except that one parameter is Callable and the other is Runnable. Slightly analyze the third (scheduleAtFixedRate) and fourth (scheduleWithFixedDelay) methods, add initialDelay = 5, period/delay = 3, and unit is seconds. If each thread runs very well and there is no delay, the running cycles of these two methods are 5, 8, 11, 14, 17... But what if there is delay? For example, the third thread takes 5 seconds. What are the processing strategies of these two methods? The third method (scheduleAtFixedRate) has a fixed cycle, that is, it will not be affected by this delay. The scheduling cycle of each thread is absolute when it is initialized. When it is scheduled, it will not be affected by the scheduling failure delay of the previous thread. However, the fourth method (scheduleWithFixedDelay) is different. It is that the scheduling interval of each thread is fixed, that is, the interval between the first thread and the second thread is delayed, the interval between the second thread and the third thread is delayed, and so on. If the second thread is delayed, all subsequent thread scheduling will be delayed. For example, if the second thread is delayed for 2 seconds, the third thread will no longer be executed for 11 seconds, but for 13 seconds.
Looking at the source code of the four methods, we will find that their processing logic is similar, so we choose the scheduleWithFixedDelay method for analysis, as follows:
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (delay <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); return t; }
The processing logic of scheduleWithFixedDelay method is as follows:
- Verify that an exception is thrown if the parameter does not conform to the law
- Construct a task, which is ScheduledFutureTask
- Call the delayedExecute() method for subsequent related processing
This code involves two classes, ScheduledFutureTask and RunnableScheduledFuture. Needless to say, RunnableScheduledFuture inherits the two interfaces of RunnableFuture and ScheduledFuture. In addition to the features of RunnableFuture and ScheduledFuture, it also defines a method isPeriodic(), which is used to judge whether the task to be executed is a periodic task, Returns true if yes. As the internal class of ScheduledThreadPoolExecutor, ScheduledFutureTask plays an extremely important role because it is responsible for the scheduling of tasks in ScheduledThreadPoolExecutor.
ScheduledFutureTask internally inherits FutureTask and implements the RunnableScheduledFuture interface. It internally defines three important variables
/** The sequence number in which the task is added to the ScheduledThreadPoolExecutor */ private final long sequenceNumber; /** Specific time of task execution */ private long time; /** Task interval */ private final long period;
These three variables are closely related to task execution. What is the relationship? Let's first look at several constructors and core methods of ScheduledFutureTask:
ScheduledFutureTask(Runnable r, V result, long ns) { super(r, result); this.time = ns; this.period = 0; this.sequenceNumber = sequencer.getAndIncrement(); } ScheduledFutureTask(Runnable r, V result, long ns, long period) { super(r, result); this.time = ns; this.period = period; this.sequenceNumber = sequencer.getAndIncrement(); } ScheduledFutureTask(Callable<V> callable, long ns) { super(callable); this.time = ns; this.period = 0; this.sequenceNumber = sequencer.getAndIncrement(); } ScheduledFutureTask(Callable<V> callable, long ns) { super(callable); this.time = ns; this.period = 0; this.sequenceNumber = sequencer.getAndIncrement(); }
ScheduledFutureTask provides four construction methods. Do these construction methods correspond to the above three parameters one by one? What and how these parameters are used depends on which methods ScheduledFutureTask uses. There is a compareTo() method in ScheduledFutureTask:
public int compareTo(Delayed other) { if (other == this) // compare zero if same object return 0; if (other instanceof ScheduledFutureTask) { ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other; long diff = time - x.time; if (diff < 0) return -1; else if (diff > 0) return 1; else if (sequenceNumber < x.sequenceNumber) return -1; else return 1; } long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; }
I believe you all know why this method is used. We provide a sorting algorithm. The rule of the algorithm is: first sort according to time, with the small time in the front and the large one in the back. If the time is the same, use sequenceNumber to sort, with the small one in the front and the large one in the back. So why provide the compareTo() method in this class? As mentioned earlier, the ScheduledThreadPoolExecutor provides a DelayedWorkQueue() queue in the construction method, that is, the ScheduledThreadPoolExecutor adds tasks to the DelayedWorkQueue, which is similar to DelayQueue. It internally maintains a queue in chronological order, so compareTo() Method uses the same algorithm as the DelayedWorkQueue queue to sort its element ScheduledThreadPoolExecutor task.
Sorting has been solved, so how does the ScheduledThreadPoolExecutor schedule and delay task tasks? Any thread is executed through the run() method. The run() method of ScheduledThreadPoolExecutor is as follows:
public void run() { boolean periodic = isPeriodic(); if (!canRunInCurrentRunState(periodic)) cancel(false); else if (!periodic) ScheduledFutureTask.super.run(); else if (ScheduledFutureTask.super.runAndReset()) { setNextRunTime(); reExecutePeriodic(outerTask); } }
- Call isPeriodic() to get the thread if it is a periodic task flag, and then call the canRunInCurrentRunState() method to determine whether the thread can be executed. If it can not be executed, call cancel() to cancel the task.
- If the thread has reached the execution point, it calls the run() method to execute the task, which is defined in FutureTask.
- Otherwise, call runAndReset() method to run and recharge, call setNextRunTime() method to calculate the next execution time of the task, and add the task to the queue again so that the task can be executed repeatedly.
isPeriodic()
This method is used to judge whether the specified task is a periodic task.
public boolean isPeriodic() { return period != 0; }
Cancelincurrentrunstate() determines whether a task can be cancelled and cancel() cancels the task. These two methods are relatively simple. run() executes the task and runAndReset() runs and resets the state. They involve a wide range of issues. We will introduce them later in FutureTask. Therefore, we will focus on setNextRunTime() and reExecutePeriodic(), two methods involving delay.
setNextRunTime()
The setNextRunTime() method is used to recalculate the next execution time of the task. As follows:
private void setNextRunTime() { long p = period; if (p > 0) time += p; else time = triggerTime(-p); }
The method definition is very simple, P > 0, time + = P, otherwise call the triggerTime() method to recalculate time:
long triggerTime(long delay) { return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)); }
reExecutePeriodic
void reExecutePeriodic(RunnableScheduledFuture<?> task) { if (canRunInCurrentRunState(true)) { super.getQueue().add(task); if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else ensurePrestart(); } }
reExecutePeriodic is important to call super.getQueue().add(task); Add the task to the DelayedWorkQueue.
Here, the ScheduledFutureTask has been introduced. The importance of ScheduledFutureTask in ScheduledThreadPoolExecutor is self-evident. In fact, the implementation of ScheduledThreadPoolExecutor is not very complex, because it is not so difficult with the support of FutureTask and ThreadPoolExecutor.