Timed Task for Concurrent Programming - Timed Thread Pool

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.

Tags: Java

Posted on Mon, 18 Oct 2021 12:30:09 -0400 by Chris.P