Principle and source code interpretation of Executor thread pool in concurrent programming

Posted by jonny 5ive on Mon, 22 Nov 2021 17:24:03 +0100

catalogue

thread

Synergetic process

Thread pool

Introduction to thread pool

Implementation of thread

Executor framework

Thread pool key properties

ctl related methods

There are five states in the thread pool

Implementation of thread pool

ThreadPoolExecutor

Creation of thread pool

Task submission

Parameter interpretation

Thread pool monitoring

Thread pool principle

  Source code analysis:

execute method

The execution process of execute method is as follows:

addWorker method

Worker class

runWorker method

getTask method

processWorkerExit method

summary

thread

       Thread is the smallest unit for scheduling CPU resources. The thread model is divided into KLT model and ULT model, KLT model used by JVM, and java thread and OS thread maintain a 1:1 mapping relationship, that is, a java thread will also have a corresponding thread in the operating system. Java threads have multiple lifestates

NEW, NEW

RUNNABLE, run

BLOCKED, BLOCKED

WAITING, WAITING

TIMED_WAITING, timeout

TERMINATED

The status switching is shown in the following figure:

Synergetic process

       The purpose of cooperative process (fiber process, user level thread) is to maximize the performance of hardware and improve the speed of software. The basic principle of cooperative process is: hang the current task at a certain point, save the stack information and execute another task; After completing or reaching a certain condition, restore the original stack information and continue execution (the whole process thread does not need context switching).

Java Native does not support coroutines. If you need to use coroutines in pure java code, you need to introduce third-party packages, such as quasar

Thread pool

"Thread pool", as its name implies, is a thread cache. Threads are scarce resources. If they are created without restrictions, they will not only consume system resources, but also reduce the stability of the system. Therefore, Java provides a thread pool to uniformly allocate, tune and monitor threads

Introduction to thread pool

In web development, the server needs to accept and process requests, so it will allocate a thread to process a request. If a new thread is created for each request, it is very easy to implement, but there is a problem:

If the number of concurrent requests is very large, but the execution time of each thread is very short, threads will be created and destroyed frequently, which will greatly reduce the efficiency of the system. It is possible that the server spends more time and system resources on creating new threads and destroying threads for each request than it does on processing actual user requests.

So is there a way to complete one task without being destroyed, but can continue to perform other tasks?

This is the purpose of thread pool. Thread pool provides a solution to the overhead and insufficient resources of thread life cycle. By reusing threads for multiple tasks, the cost of thread creation is allocated to multiple tasks.

When do I use thread pools?

  • The processing time of a single task is relatively short
  • The number of tasks to be processed is large

Thread pool advantage

  • Reuse existing threads, reduce the overhead of thread creation and extinction, and improve performance
  • Improve response speed. When the task arrives, the task can be executed immediately without waiting for the thread to be created.
  • Improve thread manageability. Threads are scarce resources. If they are created without restrictions, they will not only consume system resources, but also reduce the stability of the system. Using thread pool can be used for unified allocation, tuning and monitoring.

Implementation of thread

Runnable,Thread,Callable

// The class that implements the Runnable interface will be executed by Thread, representing a basic task
public interface Runnable {
    // The run method is all its contents, which is the task actually executed
    public abstract void run();
}
//Callable is also a task. The difference from the Runnable interface is that it receives generics and carries the returned content after executing the task
public interface Callable<V> {
    // call method with return value relative to run method
    V call() throws Exception;
}

Executor framework

The Executor interface is the most basic part of the thread pool framework. It defines an execute method for executing Runnable.

The following figure shows its inheritance and implementation

As can be seen from the figure, there is an important sub interface ExecutorService under Executor, which defines the specific behavior of thread pool

1. execute (Runnable command): perform tasks of Ruannable type,

2. Submit (task): it can be used to submit a Callable or Runnable task and return the Future object representing the task

3. shutdown (): close the work after completing the submitted task and no longer take over the new task,

4. Shutdown now (): stop all tasks being performed and close the work.

5. isTerminated(): test whether all tasks have been completed.

6. isShutdown(): test whether the ExecutorService has been closed.

Thread pool key properties

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static final int COUNT_BITS = Integer.SIZE - 3;

private static final int CAPACITY = (1 << COUNT_BITS) - 1;

ctl is a field that controls the running state of the thread pool and the number of effective threads in the thread pool. It contains two parts of information: the running state of the thread pool (runState) and the number of effective threads in the thread pool (workerCount). It can be seen here that Integer type is used to save. The upper 3 bits save runState and the lower 29 bits save workerCount. COUNT_BITS is 29, and capability is 1. Shift 29 bits to the left minus 1 (29 ones). This constant represents the upper limit of workerCount, which is about 500 million.

ctl related methods

private static int runStateOf(int c) { return c & ~CAPACITY; }

private static int workerCountOf(int c) { return c & CAPACITY; }

private static int ctlOf(int rs, int wc) { return rs | wc; }
  • runStateOf: get the running state;
  • workerCountOf: get the number of active threads;
  • ctlOf: get the values of running status and number of active threads.

There are five states in the thread pool

RUNNING    = -1 << COUNT_BITS; //The upper 3 bits are 111
SHUTDOWN   =  0 << COUNT_BITS; //The upper 3 digits are 000
STOP       =  1 << COUNT_BITS; //The upper 3 digits are 001
TIDYING    =  2 << COUNT_BITS; //The upper 3 digits are 010
TERMINATED =  3 << COUNT_BITS; //The upper 3 bits are 011

1,RUNNING

(1) Status description: when the thread pool is in RUNNING status, it can receive new tasks and process added tasks.  

(02) state switching: the initialization state of the thread pool is RUNNING. In other words, once the thread pool is created, it is in the RUNNING state, and the number of tasks in the thread pool is 0!

2, SHUTDOWN

(1) Status description: when the thread pool is in SHUTDOWN status, it does not receive new tasks, but can process added tasks.  

(2) State switching: when calling the shutdown() interface of the thread pool, the thread pool is changed from running - > shutdown.

3,STOP

(1) Status description: when the thread pool is in STOP status, it will not receive new tasks, will not process added tasks, and will interrupt the tasks being processed.  

(2) State switching: when calling the shutdown now () interface of the thread pool, the thread pool is controlled by (running or shutdown) - > stop.

4,TIDYING

(1) Status description: when all tasks are terminated and the "number of tasks" recorded in ctl is 0, the thread pool will change to TIDYING status. When the thread pool becomes TIDYING, the hook function terminated() is executed. Terminated() is empty in the ThreadPoolExecutor class. If the user wants to change the thread pool to TIDYING, carry out corresponding processing; This can be achieved by overloading the terminated () function.

(2) State switching: when the thread pool is in SHUTDOWN state, the blocking queue is empty and the tasks executed in the thread pool are also empty, it will be switched by SHUTDOWN - > tidying. When the thread pool is in the STOP state and the tasks executed in the thread pool are empty, it will be stopped - > tidying.

5, TERMINATED

(1) Status description: when the thread pool is completely TERMINATED, it becomes TERMINATED.  

(2) State switching: when the thread pool is in TIDYING state, after executing terminated(), it will be changed from TIDYING - > terminated.

The conditions for entering TERMINATED are as follows:

  • Thread pool is not in RUNNING state;
  • Thread pool state is not TIDYING state or TERMINATED state;
  • If the thread pool status is SHUTDOWN and the workerQueue is empty;
  • workerCount is 0;
  • Setting TIDYING status succeeded.

Implementation of thread pool

ThreadPoolExecutor default thread pool

ScheduledThreadPoolExecutor timed thread pool

ThreadPoolExecutor

Creation of thread pool

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

Task submission

1,public void execute() //The submitted task has no return value

2,public Future submit() //There is a return value after the task is completed

Parameter interpretation

corePoolSize

The number of core threads in the thread pool. When a task is submitted, the thread pool creates a new thread to execute the task until the current number of threads is equal to corePoolSize; If the current number of threads is corePoolSize, the tasks that continue to be submitted are saved to the blocking queue and wait to be executed; If the prestartAllCoreThreads() method of the thread pool is executed, the thread pool will create and start all core threads in advance.

maximumPoolSize

The maximum number of threads allowed in the thread pool. If the current blocking queue is full and the task continues to be submitted, a new thread will be created to execute the task, provided that the current number of threads is less than maximumPoolSize;

keepAliveTime

The thread pool maintains the idle time allowed by threads. When the number of threads in the thread pool is greater than the corePoolSize, if no new task is submitted, the threads outside the core thread will not be destroyed immediately, but will wait until the waiting time exceeds keepAliveTime;

unit

Unit of keepAliveTime;

workQueue

It is used to save the blocking queue of the task waiting to be executed, and the task must implement the Runable interface. The following blocking queue is provided in the JDK:

  • 1. ArrayBlockingQueue: bounded blocking queue based on array structure, sorting tasks by FIFO;
  • 2. Linkedblockingqueue: a blocking queue based on linked list structure. Tasks are sorted by FIFO. The throughput is usually higher than that of arrayblockingqueue;
  • 3. Synchronous queue: a blocking queue that does not store elements. Each insert operation must wait until another thread calls the remove operation. Otherwise, the insert operation is always blocked, and the throughput is usually higher than that of linkedblockingqueue;
  • 4. Priorityblockingqueue: unbounded blocking queue with priority;

threadFactory

It is a variable of type ThreadFactory used to create a new thread. By default, Executors.defaultThreadFactory() is used to create threads. When a thread is created using the default ThreadFactory, the newly created thread will have the same NORM_PRIORITY priority and is a non daemon thread. It also sets the name of the thread.

handler

The saturation strategy of the thread pool. When the blocking queue is full and there are no idle working threads, if you continue to submit a task, you must adopt a strategy to process the task. The thread pool provides four strategies:

  • 1. AbortPolicy: throw an exception directly, default policy;
  • 2. CallerRunsPolicy: use the thread of the caller to execute the task;
  • 3. DiscardOldestPolicy: discards the top task in the blocking queue and executes the current task;
  • 4. DiscardPolicy: directly discard the task;

The above four strategies are internal classes of ThreadPoolExecutor.

Of course, the RejectedExecutionHandler interface can also be implemented according to the application scenario to customize the saturation strategy, such as logging or persistent storage of tasks that cannot be processed.

Thread pool monitoring

public long getTaskCount() //The total number of tasks executed and not executed by the thread pool
public long getCompletedTaskCount() //Number of tasks completed
public int getPoolSize() //The current number of threads in the thread pool
public int getActiveCount() //The number of threads executing tasks in the thread pool

Thread pool principle

  Source code analysis:

execute method

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
/*
 * clt runState and workerCount are recorded
 */
    int c = ctl.get();
/*
 * workerCountOf Method takes the value of the lower 29 bits, indicating the number of currently active threads;
 * If the current number of active threads is less than corePoolSize, create a new thread and put it into the thread pool;
 * And add the task to the thread.
 */
    if (workerCountOf(c) < corePoolSize) {
/*
 * addWorker The second parameter in indicates whether to limit the number of added threads according to corePoolSize or maximumPoolSize;
 * If true, judge according to corePoolSize;
 * If it is false, it is determined according to the maximumPoolSize
 */
        if (addWorker(command, true))
            return;
/*
 * If the addition fails, get the ctl value again
 */
        c = ctl.get();
    }
/*
 * If the current thread pool is running and the task is added to the queue successfully
 */
    if (isRunning(c) && workQueue.offer(command)) {
// Retrieve ctl value
        int recheck = ctl.get();
 // Judge the running status of the thread pool again. If it is not running, because the command has been added to the workQueue before,
// You need to remove the command
// After execution, the handler uses the reject policy to process the task, and the whole method returns
        if (! isRunning(recheck) && remove(command))
            reject(command);
/*
 * Get the number of valid threads in the thread pool. If the number is 0, execute the addWorker method
 * The parameters passed in here represent:
 * 1. The first parameter is null, which means that a thread is created in the thread pool but not started;
 * 2. The second parameter is false. Set the upper limit of the limited number of threads in the thread pool to maximumPoolSize. When adding threads, judge according to maximumPoolSize;
 * If it is judged that workerCount is greater than 0, it will be returned directly, and the new command in workQueue will be executed at some time in the future.
 */
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
/*
 * If this is the case, there are two situations:
 * 1. Thread pool is no longer RUNNING;
 * 2. The thread pool is RUNNING, but workercount > = corepoolsize and workQueue is full.
 * At this time, the addWorker method is called again, but the second parameter is passed in as false, and the upper limit of the limited number of threads in the thread pool is set to maximumPoolSize;
 * Reject the task if it fails
 */
    else if (!addWorker(command, false))
        reject(command);
}

Simply put, when executing the execute() method, if the status is always RUNNING, the execution process is as follows:

  1. If workercount < corepoolsize, create and start a thread to execute the newly submitted task;
  2. If workercount > = corepoolsize and the blocking queue in the thread pool is not full, add the task to the blocking queue;
  3. If workercount > = corepoolsize & & workercount < maximumpoolsize, and the blocking queue in the thread pool is full, create and start a thread to execute the newly submitted task;
  4. If workercount > = maximumpoolsize and the blocking queue in the thread pool is full, the task is processed according to the rejection policy. The default processing method is to throw an exception directly.

Note that addWorker(null, false);, That is, a thread is created, but no task is passed in. Because the task has been added to the workQueue, the worker will directly obtain the task from the workQueue when executing. Therefore, when workerCountOf(recheck) == 0, execute addWorker(null, false); In order to ensure that the thread pool is in the RUNNING state, there must be a thread to execute tasks.

The execution process of execute method is as follows:

addWorker method

       The main work of addWorker method is to create a new thread in the thread pool and execute it. The firstTask parameter is used to specify the first task executed by the new thread. The core parameter is true, which means that when adding a thread, it will judge whether the current number of active threads is less than corePoolSize, and false, which means that it is necessary to judge whether the current number of active threads is less than maximumPoolSize before adding a thread, The code is as follows:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
    // Get running status
        int rs = runStateOf(c);
    /*
     * This if judgment
     * If RS > = shutdown, it means that new tasks will not be received at this time;
     * Then judge the following three conditions. As long as one of them is not satisfied, false will be returned:
     * 1. rs == SHUTDOWN,At this time, it indicates that it is closed and will no longer accept the newly submitted tasks, but it can continue to process the saved tasks in the blocking queue
     * 2. firsTask Empty
     * 3. Blocking queue is not empty
     * 
     * First, consider the case of rs == SHUTDOWN
     * In this case, the newly submitted task will not be accepted, so false will be returned when the firstTask is not empty;
     * Then, if firstTask is empty and workQueue is also empty, false is returned,
     * Because there are no tasks in the queue, there is no need to add threads
     */
     // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                        firstTask == null &&
                        ! workQueue.isEmpty()))
            return false;
        for (;;) {
            // Get number of threads
            int wc = workerCountOf(c);
            // If wc exceeds capability, that is, the maximum value of the lower 29 bits of ctl (binary is 29 1), false is returned;
            // The core here is the second parameter of the addWorker method. If true, it means to compare according to the corePoolSize,
            // If false, the comparison is based on maximumPoolSize.
            // 
            if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // Try to increase the workerCount, and if successful, jump out of the first for loop
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // If increasing workerCount fails, retrieve the ctl value
            c = ctl.get();  // Re-read ctl
            // If the current running state is not equal to rs, it indicates that the state has been changed. Return to the first for loop to continue execution
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
     // Create a Worker object based on firstTask
        w = new Worker(firstTask);
     // Each Worker object creates a thread
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());
                // RS < shutdown indicates RUNNING status;
                // If rs is RUNNING or rs is SHUTDOWN and firstTask is null, add a thread to the thread pool.
                // Because new tasks will not be added during SHUTDOWN, but the tasks in workQueue will still be executed
                if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // workers is a HashSet
                    workers.add(w);
                    int s = workers.size();
                    // largestPoolSize records the maximum number of threads in the thread pool
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // Start thread
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

Worker class

Each thread in the thread pool is encapsulated into a Worker object. ThreadPool actually maintains a group of Worker objects. See JDK source code.

The Worker class inherits AQS and implements the Runnable interface. Note the firstTask and thread attributes: firstTask uses it to save the incoming task; Thread is a thread created through ThreadFactory when calling the construction method. It is a thread used to process tasks.

When calling the construction method, you need to pass in the task through getThreadFactory().newThread(this); To create a new thread. The parameter passed in by the newThread method is this. Because the Worker itself inherits the Runnable interface, that is, a thread, a Worker object will call the run method in the Worker class when it is started.

Worker inherits AQS and uses AQS to realize the function of exclusive lock. Why not use ReentrantLock? You can see the tryAcquire method, which does not allow reentry, while ReentrantLock allows reentry:

  1. Once the lock method obtains the exclusive lock, it indicates that the current thread is executing a task;
  2. If the task is executing, the thread should not be interrupted;
  3. If the thread is not in the exclusive lock state, that is, the idle state, it indicates that it is not processing a task. At this time, the thread can be interrupted;
  4. When the thread pool executes the shutdown method or tryTerminate method, it will call the interruptIdleWorkers method to interrupt idle threads. The interruptIdleWorkers method will use the tryLock method to judge whether the threads in the thread pool are idle;
  5. The reason why it is set to non reentrant is that we do not want the task to re acquire the lock when calling a thread pool control method such as setcorepoolsize. If ReentrantLock is used, it can be reentrant, so if the thread pool control method such as setCorePoolSize is called in the task, it will interrupt the running thread.

Therefore, Worker inherits from AQS and is used to determine whether a thread is idle and can be interrupted.

In addition, setState(-1) is executed in the constructor;, Set the state variable to - 1. Why? This is because the default state in AQS is 0. If a Worker object has just been created and the task has not been executed, it should not be interrupted. Take a look at the tryAquire method:

protected boolean tryAcquire(int unused) {
//cas modifies state and cannot re-enter
    if (compareAndSetState(0, 1)) { 
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

The tryAcquire method determines whether the state is 0. Therefore, setState(-1); The state is set to - 1 to prevent the thread from being interrupted before executing the task.

Because of this, in the runWorker method, the unlock method of the Worker object will be called first to set the state to 0.

runWorker method

The run method in the Worker class calls the runWorker method to execute the task. The code of the runWorker method is as follows:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // Get the first task
    Runnable task = w.firstTask;
    w.firstTask = null;
    // Allow interrupt
    w.unlock(); // allow interrupts
    // Is the loop exited because of an exception
    boolean completedAbruptly = true;
    try {
        // If the task is empty, get the task through getTask
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

Here is the first if judgment. The purpose is:

  • If the thread pool is stopping, ensure that the current thread is in an interrupted state;
  • If not, ensure that the current thread is not in interrupt state;

Here, we should consider that the shutdown now method may also be executed during the execution of the if statement. The shutdown now method will set the state to STOP. Review the STOP state:

Unable to accept new tasks or process tasks in the queue will interrupt the thread processing tasks. When the thread pool is in the RUNNING or SHUTDOWN state, calling the shutdownNow() method will bring the thread pool into this state.

The STOP state interrupts all threads in the thread pool. Here, Thread.interrupted() is used to determine whether to interrupt to ensure that the thread is in a non interrupted state in the RUNNING or SHUTDOWN state, because the Thread.interrupted() method will reset the interrupted state.

Summarize the execution process of runWorker method:

  1. The while loop continuously obtains tasks through the getTask() method;
  2. getTask() method fetches the task from the blocking queue;
  3. If the thread pool is stopping, ensure that the current thread is in interrupt state; otherwise, ensure that the current thread is not in interrupt state;
  4. Call task.run() to execute the task;
  5. If the task is null, jump out of the loop and execute the processWorkerExit() method;
  6. The execution of the runWorker method also means that the execution of the run method in the Worker is completed and the thread is destroyed.

The beforeExecute method and afterExecute method here are empty in the ThreadPoolExecutor class and are left to be implemented by subclasses.

The completedAbruptly variable indicates whether an exception occurs during task execution. The value of this variable will be judged in the processWorkerExit method.

getTask method

The getTask method is used to fetch tasks from the blocking queue. The code is as follows:

private Runnable getTask() {
    // The value of the timeOut variable indicates whether the task timed out the last time it was fetched from the blocking queue
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
    /*
     * If the thread pool status RS > = shutdown, that is, non RUNNING status, make the following judgment:
     * 1. rs >= STOP,Whether the thread pool is stop ping;
     * 2. Whether the blocking queue is empty.
     * If the above conditions are met, the workerCount is subtracted by 1 and null is returned.
     * Because if the value of the current thread pool state is SHUTDOWN or above, it is not allowed to add tasks to the blocking queue.
     */
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // Are workers subject to culling?
        // The timed variable is used to determine whether timeout control is required.
        // allowCoreThreadTimeOut is false by default, that is, the core thread is not allowed to timeout;
        // WC > corepoolsize, indicating that the number of threads in the current thread pool is greater than the number of core threads;
        // For these threads that exceed the number of core threads, timeout control is required
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

    /*
     * wc > maximumPoolSize The reason is that the setMaximumPoolSize method may be executed at the same time in this method execution phase;
     * timed && timedOut If true, it indicates that the current operation needs timeout control, and the last time the task was obtained from the blocking queue timed out
     * Next, judge that if the number of valid threads is greater than 1 or the blocking queue is empty, try to reduce the workerCount by 1;
     * If subtraction by 1 fails, retry is returned.
     * If wc == 1, it means that the current thread is the only thread in the thread pool.
     */
        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
        /*
         * Judge according to timed. If it is true, the timeout is controlled through the poll method of blocking the queue. If the task is not obtained within the keepAliveTime time, null is returned;
         * Otherwise, through the take method, if the queue is empty at this time, the take method will block until the queue is not empty.
         *
         */
            Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
            if (r != null)
                return r;
            // If r == null, it indicates that it has timed out, and timedOut is set to true
            timedOut = true;
        } catch (InterruptedException retry) {
            // If the current thread is interrupted when obtaining the task, set timedOut to false and return to loop retry
            timedOut = false;
        }
    }
}

       The important part here is the second if judgment, which aims to control the effective number of threads in the thread pool. As can be seen from the above analysis, when executing the execute method, if the number of threads in the current thread pool exceeds corePoolSize and is less than maximumPoolSize, and the workQueue is full, you can increase the number of working threads. However, if no task is obtained during timeout, that is, when timedOut is true, it indicates that the workQueue is empty, This means that there are not so many threads in the current thread pool to execute tasks. You can destroy more threads than corePoolSize and keep the number of threads in corePoolSize.

When will it be destroyed? Of course, after the runWorker method is executed, that is, after the run method in the Worker is executed, it is automatically recycled by the JVM.

When the getTask method returns null, the while loop will jump out of the runWorker method, and then the processWorkerExit method will be executed.

processWorkerExit method

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // If the completedAbruptly value is true, an exception occurs during thread execution, and the workerCount needs to be reduced by 1;
    // If there is no exception during thread execution, it indicates that the workerCount has been reduced by 1 in the getTask() method. There is no need to reduce it here.  
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //Count the number of tasks completed
        completedTaskCount += w.completedTasks;
        // Removing from workers means that a worker thread is removed from the thread pool
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
    // Judge whether to end the thread pool according to the thread pool status
    tryTerminate();
    int c = ctl.get();
/*
 * When the thread pool is in the RUNNING or SHUTDOWN state, if the worker ends abnormally, it will directly add the worker;
 * If allowCoreThreadTimeOut=true and there are tasks in the waiting queue, at least one worker shall be reserved;
 * If allowCoreThreadTimeOut=false, workerCount is not less than corePoolSize.
 */
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

So far, after the processWorkerExit is executed, the working thread is destroyed. The above is the life cycle of the whole working thread. Starting from the execute method, the Worker uses ThreadFactory to create a new working thread. runWorker obtains the task through getTask, and then executes the task. If getTask returns null, enter the processWorkerExit method, and the whole thread ends, as shown in the figure:

summary

  • The creation of thread, the submission of task, the transformation of state and the closure of thread pool are analyzed;
  • Here, the workflow of the thread pool is expanded through the execute method. The execute method determines whether the incoming task should be executed immediately, added to the blocking queue, or rejected through corePoolSize, maximumPoolSize, and the size of the blocking queue.
  • The process of thread pool closing is introduced, and the race condition between shutdown method and getTask method is also analyzed;
  • When obtaining a task, it is necessary to judge whether the worker thread should end or block the thread to wait for a new task through the state of the thread pool. It also explains why the worker thread should be interrupted when the thread pool is closed and why every worker needs lock.

Topics: Java thread pool