Deep analysis of thread pool in java thread series

(it's more convenient to see the source code on the horizontal screen of mobile phone)

Note: java source code analysis part is based on java version 8 unless otherwise specified.

Note: This article is based on the ScheduledThreadPoolExecutor timed thread pool class.

brief introduction

We have learned the execution process of common tasks and future tasks together. Today, we will learn a new task - timed task.

Timed task is a kind of task that we often use. It represents the task to be executed at a certain time in the future or to be executed repeatedly according to some rules in the future.

problem

(1) how to ensure that the task is performed at a certain time in the future?

(2) how to ensure that tasks are executed repeatedly according to certain rules?

Here comes a chestnut.

Create a timed thread pool to run four different timed tasks.

public class ThreadPoolTest03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Create a timed thread pool
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);

        System.out.println("start: " + System.currentTimeMillis());

        // Execute a task without return value, 5 seconds later, only once
        scheduledThreadPoolExecutor.schedule(() -> {
            System.out.println("spring: " + System.currentTimeMillis());
        }, 5, TimeUnit.SECONDS);

        // Execute a task with return value in 5 seconds, only once
        ScheduledFuture<String> future = scheduledThreadPoolExecutor.schedule(() -> {
            System.out.println("inner summer: " + System.currentTimeMillis());
            return "outer summer: ";
        }, 5, TimeUnit.SECONDS);
        // Get return value
        System.out.println(future.get() + System.currentTimeMillis());

        // Perform a task at a fixed frequency every 2 seconds and 1 second later
        // 2 seconds after the start of the task
        scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
            System.out.println("autumn: " + System.currentTimeMillis());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        }, 1, 2, TimeUnit.SECONDS);

        // Execute a task according to the fixed delay, once every 2 seconds and once every 1 second
        // Two seconds after the end of the task, this article was originally created by the official number "Tong Ge read the source code"
        scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -> {
            System.out.println("winter: " + System.currentTimeMillis());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        }, 1, 2, TimeUnit.SECONDS);
    }
}

There are four types of timing tasks:

(1) there is no return value for the task to be executed once in the future;

(2) for tasks to be executed once in the future, there is a return value;

(3) tasks to be repeated at a fixed frequency in the future;

(4) tasks to be executed repeatedly with fixed delay in the future;

This paper takes the third one as an example to analyze the source code.

scheduleAtFixedRate() method

Submit a task to be performed at a fixed frequency.

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    // Parameter judgement
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
        
    // Decorate normal tasks with ScheduledFutureTask
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period));
    // Hook method, which is used by subclass to replace decoration task. Here, t==sft
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    // Delayed execution
    delayedExecute(t);
    return t;
}

It can be seen that the processing here is similar to the future tasks, which are decorated as another task and then executed. The difference is that the delayedExecute() method is given to execute. What is this method for?

delayedExecute() method

Delay execution.

private void delayedExecute(RunnableScheduledFuture<?> task) {
    // If the thread pool is closed, execute the deny policy
    if (isShutdown())
        reject(task);
    else {
        // Put the task in the queue first
        super.getQueue().add(task);
        // Check thread pool status again
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
            // Ensure that there are enough threads to perform tasks
            ensurePrestart();
    }
}
void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    // Create worker thread
    // Note that the firstTask parameter is not passed in here, because the task is thrown into the queue first
    // In addition, the maxPoolSize parameter is not used, so the maximum number of threads is not used in the timed thread pool
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

It's over here?!

In fact, this is just to control whether the task can be executed. The real place to execute the task is in the run() method of the task.

Remember that the task above was decorated as an instance of the ScheduledFutureTask class? So, all we have to do is look at the run() method of ScheduledFutureTask.

The run() method of the ScheduledFutureTask class

Where tasks are scheduled.

public void run() {
    // Repeat or not
    boolean periodic = isPeriodic();
    // Thread pool state judgment
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // One time task, directly call the run() method of the parent class, which is actually FutureTask
    // We will not explain it here any more. If you are interested, please take a look at the content of the previous chapter
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // For repetitive tasks, first call the runAndReset() method of the parent class, which is also FutureTask
    // This paper mainly analyzes the following parts
    else if (ScheduledFutureTask.super.runAndReset()) {
        // Set next execution time
        setNextRunTime();
        // Repeated execution, this article is original by the public subordinate number "tongge read source code"
        reExecutePeriodic(outerTask);
    }
}

As you can see, for repetitive tasks, first call the runAndReset() method of FutureTask, then set the next execution time, and finally call the reExecutePeriodic() method.

FutureTask's runAndReset() method is similar to the run() method, except that the status will not be changed to NORMAL after the task is completed. Interested students can click the source code to have a look.

Let's look at the reExecutePeriodic() method.

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    // Thread pool status check
    if (canRunInCurrentRunState(true)) {
        // Throw the task into the task queue again
        super.getQueue().add(task);
        // Check thread pool status again
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
            // Ensure enough worker threads
            ensurePrestart();
    }
}

Is it suddenly clear that the scheduled thread pool performs repeated tasks after the task is executed and then throws the task back into the task queue.

The repetitive problem is solved, so how does it control the execution of tasks at a certain time?

OK, it's our turn to delay the queue.

DelayedWorkQueue inner class

We know that when the thread pool performs tasks, it needs to take tasks out of the task queue, while the common task queue, if there are tasks in it, can take them out directly, but the delay queue is different. The tasks in it can't be taken out if they don't arrive at the time, which is the reason why tasks are put into the queue in the previous analysis and the creation Worker doesn't pass into the firstTask.

After all, how does it come true?

In fact, we have analyzed the delay queue in detail before. If you want to see the complete source code analysis, you can see the previous "DelayQueue source code analysis of kowtow java collection".

The data structure of "heap" is used to implement the delay queue. Students who are interested in it can look at the previous "please don't ask me about the heap (sorting) in the interview!"! "

We only take a take() method to analyze.

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // Lock up
    lock.lockInterruptibly();
    try {
        for (;;) {
            // Pile top task
            RunnableScheduledFuture<?> first = queue[0];
            // If the queue is empty, wait
            if (first == null)
                available.await();
            else {
                // How long will it take
                long delay = first.getDelay(NANOSECONDS);
                // If it is less than or equal to 0, it means that the task is up to time and can be queued out of the queue
                if (delay <= 0)
                    // Get out of the line and pile up
                    return finishPoll(first);
                // It's not time yet
                first = null;
                // If there is a thread waiting ahead, enter the wait directly
                if (leader != null)
                    available.await();
                else {
                    // Current thread as leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // Wait for the delay time calculated above, and then wake up automatically
                        available.awaitNanos(delay);
                    } finally {
                        // After waking up, get the lock again, and then empty the leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && queue[0] != null)
            // Equivalent to waking up the next waiting task
            available.signal();
        // Unlocking, this article is originally created by the public subordinate number "Tong Ge read the source code"
        lock.unlock();
    }
}

The general principle is to use the characteristics of the heap to get the tasks as fast as time, that is, the tasks at the top of the heap:

(1) if the task on the top of the heap is up to time, it will be queued from the queue;

(2) if the task on the top of the reactor has not reached the time, it depends on how long it has to reach the time. Use the conditional lock to wait for this period of time, and then walk again (1);

This solves the problem that tasks can be executed after a specified time.

Other

In fact, ScheduledThreadPoolExecutor can also use execute() or submit() to submit tasks, but they will be executed once as 0-delay tasks.

public void execute(Runnable command) {
    schedule(command, 0, NANOSECONDS);
}
public <T> Future<T> submit(Callable<T> task) {
    return schedule(task, 0, NANOSECONDS);
}

summary

There are two problems to be solved in the realization of timed tasks, one is to assign tasks to be executed at a certain time in the future and the other is to repeat them.

(1) the task execution at a certain time is solved by the characteristics of delay queue;

(2) repeat execution is solved by adding tasks to the queue again after task execution.

Egg

So far, the source code analysis of the common thread pool is over. This thread pool is a more classic implementation. On the whole, the efficiency is not particularly high, because all the working threads share the same queue, and each time you take a task from the queue, you need to lock and unlock it.

Then, can each worker thread be equipped with a task queue? When the task is submitted, the task is assigned to the specified worker thread, so that when the task is fetched, there is no need for frequent locking and unlocking.

The answer is yes. In the next chapter, let's take a look at this kind of thread pool based on the theory of "work stealing" - fork join pool.

Welcome to pay attention to my public number "Tong Ge read the source code", see more source series articles, and swim together with brother Tong's source ocean.

Tags: Java Mobile Spring less

Posted on Tue, 05 Nov 2019 08:51:40 -0500 by lulubell