1. Timer Thread Pool
1.1. Origin of Timer Thread Pool
The ScheduledThreadPoolExecutor timer thread pool is inherited from the ThreadPoolExecutor class and implements the ScheduledExecutorService class.
As shown in the following figure:
1.2, api of ScheduledThreadPoolExecutor
1.2.1,schedule
The schedule api delays the execution of a task. The code in the run method will execute five seconds after the thread starts, as follows:
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); scheduledThreadPoolExecutor.schedule(new Runnable() { @Override public void run() { logger.info("I want to delay 5 s Execute!"); } },5000, TimeUnit.MILLISECONDS); }
Screenshot of program execution results:
17s = => 22s
1.2.2,scheduleAtFixedRate
It has two parameters,
The first parameter is how long the thread will delay execution after restarting
The second parameter is how long the cycle of loop execution takes
Here's what delays a second after the thread starts, then executes in a two-second cycle
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { logger.info("Execute..."); } },1000,2000,TimeUnit.MILLISECONDS); }
The execution results are shown in the following figure:
Question: What if the task takes longer to execute than the interval cycle?
Tasks are queued in a blocked queue, and when the previous task is executed, it is empty to execute the next task. The code is as follows:
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { logger.info("Execute..."); long start = System.currentTimeMillis(); while(true){ long end = System.currentTimeMillis(); if(end - start > 5000){ break; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } },1000,2000,TimeUnit.MILLISECONDS); }
The results of the execution are as follows:
Note: Although it takes 5 seconds to execute a task here, there must be a lot of tasks scheduled in the blocked queue. Even if we enlarge the thread pool, it won't help because we can only execute one task by one thread.
1.2.3,scheduleWithFixedDelay
Since many times we don't know how long a task will last, here's the api that takes us a little longer to do the next cycle after the last task has been executed. The code is as follows:
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { logger.info("Execute..."); long start = System.currentTimeMillis(); while(true){ long end = System.currentTimeMillis(); if(end - start > 5000){ break; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } },1000,2000,TimeUnit.MILLISECONDS); }
The execution screenshot is as follows:
Task basic 7s executes once
1.3. Timed Thread Pool Scenario
1. Redis Distributed Lock
If one of our Redis has many clients, when a client gets a lock and hangs up suddenly, then our lock resource will have an expiration time, which is normally satisfactory, but if our business logic is more complex, this expiration time is not enough. Then there will be other threads getting the current lock resource, so we can use this timer thread pool at this time. Our timer thread pool will check the lock every other time, and if it still exists, continue for a few seconds until the lock is released, so no matter how long you perform this task, It is guaranteed that the lock will not be released until the task is executed.
2. zk Registry and Service Discovery
If our project is a micro-service architecture, each of our systems only needs to access zk. ZK will automatically distribute the Live services according to the service name we sent them. What happens to these live services? They will send their own configuration information to ZK at regular intervals, and ZK will be managed after they get it. Subsequently, other services are assigned accordingly. This way of sending your own information at regular intervals can be done using the ScheduledThreadPoolExecutor timer thread pool.
3. Refresh the cache regularly and so on.
2. Source code analysis of timer thread pool
Let me take the scheduleAtFixedRate api as an example, and everything else is almost the same
First we get into this method
2.1,scheduleAtFixedRate
Construction method of scheduleAtFixedRate
1. Sort tasks.
2. Store tasks.
3. Delayed execution method.
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period <= 0) throw new IllegalArgumentException(); // Tasks are sorted by an internal wrapper by heap ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); // Nothing inside, left for expansion RunnableScheduledFuture<Void> t = decorateTask(command, sft); // Store current task in outerTask member variable sft.outerTask = t; // Delayed execution delayedExecute(t); return t; }
2.2,delayedExecute
Delayed Execution Method
1. Execute the rejection policy if the thread pool has been suspended.
2. Put the task in the queue and judge the state of the thread pool again. If it is OK, find the thread to execute.
private void delayedExecute(RunnableScheduledFuture<?> task) { // Execute rejection policy if thread pool is suspended if (isShutdown()) reject(task); else { // Otherwise, put the task directly in the queue super.getQueue().add(task); // Determine again if there is a problem with the state of the thread pool if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else // If there is no problem, create a thread to perform the task ensurePrestart(); } }
2.3,ensurePrestart
The main thing is to create threads to perform tasks
void ensurePrestart() { // Number of threads removed from the thread pool int wc = workerCountOf(ctl.get()); // Create a core thread to perform tasks if it is less than the number of core threads if (wc < corePoolSize) addWorker(null, true); // If the worker threads are all equal to 0 and the number of core threads is not large, then the configuration of the number of core threads can only be 0. // Walking up to this indicates that the number of core threads parameter is 0, but the task is still executing, so start a non-core thread else if (wc == 0) addWorker(null, false); // Indicates that the core thread has been created full and waits for the core thread to free up before execution because it has already been queued }
2.4. run method of ScheduledThreadPoolExecutor
A series of state judgments are made in addWorker, and the thread is finally started. Since the parameter passed in just when the thread was created is this, the run method inside ScheduledThreadPoolExecutor needs to be executed.
public void run() { // Take out the period value boolean periodic = isPeriodic(); // Determine if the current thread is in a normal state if (!canRunInCurrentRunState(periodic)) cancel(false); // If you don't do this in a cycle, just do it once else if (!periodic) ScheduledFutureTask.super.run(); // If it's cyclical, go this one, the kind of cyclical execution // Scheduled FutureTask.super.runAndReset() performs specific tasks and resets else if (ScheduledFutureTask.super.runAndReset()) { // Next Execution Time setNextRunTime(); // Prepare for next execution reExecutePeriodic(outerTask); } }
2.5,reExecutePeriodic
void reExecutePeriodic(RunnableScheduledFuture<?> task) { if (canRunInCurrentRunState(true)) { // Add to Queue super.getQueue().add(task); // Determine if the thread state is okay if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else // Continue 2.3 ensurePrestart(); } }
10. Auxiliary Knowledge
10.1. Handling of timed tasks when an exception is thrown in a task
10.1.1, Processing of ScheduledThreadPoolExecutor
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { logger.info("Execute..."); throw new RuntimeException(); } },1000,2000,TimeUnit.MILLISECONDS); }
The execution screenshots are as follows:
We'll find threads stuck here because at the bottom of the thread pool, we caught exceptions and created another thread. Although the thread pool exists and threads exist, tasks are discarded, so the display is stuck here.
Processing of 10.1.2, Timer
The code example is as follows:
private static Logger logger = Logger.getLogger(Log.class.getName()); public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); logger.info("At the beginning!"); Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { logger.info("Execute..."); throw new RuntimeException(); } },1000,2000); }
The execution screenshots are as follows:
We will find that the direct error was reported and the program stopped running because it threaded directly to a member variable, so throwing an exception caused a serious problem, so many specifications do not recommend using Timer.