On the principle of thread pool

Posted by intodesi on Tue, 21 Jan 2020 13:45:39 +0100

Status of thread pool
First let's look at some variables defined in the ThreadPoolExecutor class:

volatile int runState; //volatile is a type modifier.
					//volatile is used as an instruction key to ensure that this instruction will not be omitted due to compiler optimization.
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState: the state of the current thread pool, which is a volatile variable to ensure visibility between threads.
The following static final variables represent several possible values of runState.
1. After creating a thread pool, the thread pool is in RUNNING state initially;
2. If the shutdown() method is called, the thread pool is in SHUTDOWN state. At this time, the thread pool cannot accept new tasks, and it will wait for all tasks to be completed;
3. If the shutdownNow() method is called, the thread pool is in the STOP state. At this time, the thread pool cannot accept new tasks, and will try to terminate the executing tasks;
4. When the thread pool is in SHUTDOWN or STOP state, and all working threads have been destroyed, and the task cache queue has been emptied or the execution has ended, the thread pool is set to TERMINATED state.

Some member variables of ThreadPoolExecutor class
Let's take a look at some other important member variables in the ThreadPoolExecutor class:

private final BlockingQueue<Runnable> workQueue;        //Task cache queue, used to store tasks waiting to be executed
private final ReentrantLock mainLock = new ReentrantLock();   //Main state lock of thread pool, for thread pool
														//State (such as thread pool size, runState, etc.)
														//Use this lock for all changes
private final HashSet<Worker> workers = new HashSet<Worker>();  //Used to store worksets
 
private volatile long  keepAliveTime;    //Thread inventory time   
private volatile boolean allowCoreThreadTimeOut;   //Allow to set lifetime for core threads
private volatile int   corePoolSize;     //The size of the thread pool (that is, when the number of threads in the thread pool is greater than this parameter
										//, submitted tasks will be put into the task cache queue)
private volatile int   maximumPoolSize;   //Maximum number of threads that the thread pool can tolerate
 
private volatile int   poolSize;       //Current number of threads in the thread pool
 
private volatile RejectedExecutionHandler handler; //Task rejection policy
 
private volatile ThreadFactory threadFactory;   //Thread factory, used to create threads
 
private int largestPoolSize;   //Used to record the maximum number of threads in the thread pool
 
private long completedTaskCount;   //Used to record the number of completed tasks

The process from task submission to final execution
In the ThreadPoolExecutor class, the core task submission method is the execute() method. Although you can submit tasks through submit(), the final call in the submit() method is the execute() method, so we only need to study the implementation principle of the execute() method, so let's look at the source code of the execute() method:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();  //Determine whether the submitted task command is null,
        								  //If NULL, a null pointer exception is thrown.
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);  //To deal with the emergency, we can see from the name that it is a guarantee 
                									//Tasks added to the task cache queue are processed.
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); 
    }
}

Some of the above code words are difficult to understand. Now let's explain one by one:

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

This is an or operation. First, the relationship between poolSize and corePoolSize will be judged. If the number of threads in the thread pool is greater than or equal to the size of the thread pool, it will enter the if statement directly. If the number of threads in the thread pool is smaller than the size of the thread pool, then the addIfUnderCorePoolSize(command) method will not enter the if statement if the returned result is true , if the result returned is false, enter the if statement.

if (runState == RUNNING && workQueue.offer(command))
		```
else if (!addIfUnderMaximumPoolSize(command))
            reject(command); 
    }	

First, judge whether the state of thread pool is RUNNING. If not, directly judge the next else if, and then execute addIfUnderMaximumPoolSize(command). If the return value is true, the whole if body will jump out. If the return value is false, execute reject(command);
If so, the task will be put into the task cache queue, but if the execution of offer() fails, the next else if will also be judged directly.

if (runState != RUNNING || poolSize == 0)
	 ensureQueuedTaskHandled(command);

This is also an or operation. First, judge whether the status of the thread pool is RUNNING. If not, then judge whether the current number of threads in the thread pool is equal to 0. If so, execute the method of ensurequeuedtashandled (command). Otherwise, jump out of the current if body.

Until now, there are two methods that haven't been explained, which are addIfUnderCorePoolSize(command) and addIfUnderMaximumPoolSize(command). Let's take a look at the source code of these two methods.

addIfUnderCorePoolSize(command):

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //Create a thread to execute the first task task   
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

The whole process of the whole method is to obtain the lock first, because the change of Thread pool state is involved in this place, and then judge the current number of threads in the Thread pool, the size of the Thread pool and the state of the Thread pool. When everything is ok, execute the addThread() method. The return value of this method is a Thread type. If the Thread creation fails, then t==null; if the Thread creation fails, then t==null If the Thread succeeds, let the Thread start().
Let's look at the addThread() method:

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //Create a thread to execute the task   
    if (t != null) {
        w.thread = t;            //Assign the reference of the created thread to the member variable of w       
        workers.add(w);
        int nt = ++poolSize;     //Number of current threads plus 1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

This method is relatively easy to understand. Let's look at the source code of the worker class:

private final class Worker implements Runnable {
    private final ReentrantLock runLock = new ReentrantLock();
    private Runnable firstTask;
    volatile long completedTasks;
    Thread thread;
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
    }
    boolean isActive() {
        return runLock.isLocked();
    }
    void interruptIfIdle() {
        final ReentrantLock runLock = this.runLock;
        if (runLock.tryLock()) {
            try {
        if (thread != Thread.currentThread())
        thread.interrupt();
            } finally {
                runLock.unlock();
            }
        }
    }
    void interruptNow() {
        thread.interrupt();
    }
 
    private void runTask(Runnable task) {
        final ReentrantLock runLock = this.runLock;
        runLock.lock();
        try {
            if (runState < STOP &&
                Thread.interrupted() &&
                runState >= STOP)
            boolean ran = false;
            beforeExecute(thread, task);   //The beforeExecute method is of the ThreadPoolExecutor class
            								//One method, no specific implementation          
            try {
                task.run();
                ran = true;
                afterExecute(task, null);
                ++completedTasks;
            } catch (RuntimeException ex) {
                if (!ran)
                    afterExecute(task, ex);
                throw ex;
            }
        } finally {
            runLock.unlock();
        }
    }
 
    public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);   //Clean up when there are no tasks in the task queue       
        }
    }
}

Many people will ask us what the source code of this class means. It seems that it has little to do with the above. It only uses a construction method, but this class implements the Runnable interface, so we should focus on the run() method of this class.
The first task it executes is the first task passed in through the constructor. After the first task is executed by calling runTask(), the new tasks in the task cache queue are executed by getTask() in the while loop.
This is a very clever design method. If we design the thread pool, there may be a task dispatch thread. When we find that there are threads idle, we will take a task from the task cache queue and give it to the idle thread for execution. However, this method is not adopted here, because it will additionally manage the task dispatch thread, which will increase the difficulty and complexity invisibly. Here, the thread that has completed the task is directly asked to fetch the task from the task cache queue for execution.

addIfUnderMaximumPoolSize(command)

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < maximumPoolSize && runState == RUNNING)
            t = addThread(firstTask);
    } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

This method is similar to the previous one. The only difference is that the current number of threads in the thread pool in the if condition is compared with the maximum number of threads that the thread pool can tolerate, rather than the size of the thread pool.

To sum up, let's summarize:

  • If the number of threads in the current thread pool is less than the core pool size, a thread will be created to execute each task;
  • If the number of threads in the current thread pool is > = corepoolsize, each task will be added to the task cache queue. If it is added successfully, the task will wait for the idle thread to fetch it out for execution. If it fails to add (generally speaking, the task cache queue is full), a new thread will be created to execute the task;
  • If the number of threads in the current thread pool reaches maximumPoolSize, the task rejection policy will be adopted for processing;
  • If the number of threads in the thread pool is greater than corePoolSize, if a thread idle time exceeds keepAliveTime, the thread will be terminated until the number of threads in the thread pool is not greater than corePoolSize; if it is allowed to set the survival time for the threads in the core pool, the thread idle time in the core pool exceeds keepAliveTime, and the thread will also be terminated.

Finally, in order to facilitate your understanding, let me use a more realistic example to illustrate:
There are 10 workers in a factory. Each worker can only do one task at a time. Therefore, as long as 10 workers are free, they will be assigned tasks when they come; when 10 workers are all doing tasks, if they still come, they will queue for tasks; if the growth rate of new tasks is much faster than that of workers, then the factory director may want to take remedial measures, such as re recruiting 4 temporary workers Workers come in, and then assign tasks to the four temporary workers. If the speed of 14 workers' tasks is not enough, the plant supervisor may consider not accepting new tasks or abandoning some of the previous tasks. When some of these 14 workers are free, and the growth rate of new tasks is relatively slow, the factory director may consider to quit four temporary workers, only to keep the original 10 workers, after all, it costs money to hire additional workers.

Here 10 stands for corePoolSize and 14 stands for maximumPoolSize.

Published 7 original articles, won praise 0, visited 150
Private letter follow

Topics: less supervisor