Detailed explanation of Java thread pool, this may be the best article

In the previous article, when we use threads, we create a thread, which is very simple to implement, but there is a problem:

If there are a large number of concurrent threads, and each thread ends by executing a task for a short time, creating threads frequently will greatly reduce the efficiency of the system, because it takes time to create and destroy threads frequently.

So is there a way to make threads reusable, that is, after executing a task, it will not be destroyed, but can continue to execute other tasks?

In Java, this effect can be achieved through a thread pool. Today we will explain the thread pool of Java in detail. First, we will start with the methods in the core ThreadPoolExecutor class, then describe its implementation principle, then give an example of its use, and finally discuss how to configure it reasonably. The size of the thread pool.

1. The ThreadPoolExecutor class in Java

The java.uitl.concurrent.ThreadPoolExecutor class is the core class in the thread pool, so if you want to thoroughly understand the thread pool in Java, you must first understand this class. Let's take a look at the specific implementation source code of the ThreadPoolExecutor class.

Four constructors are provided in the ThreadPoolExecutor class:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

As can be seen from the above code, ThreadPoolExecutor inherits the AbstractExecutorService class and provides four constructors. In fact, by observing the specific implementation of the source code of each constructor, it is found that the first three constructors are the fourth constructor called initialization work performed by the device.

The following explains the meaning of each parameter in the constructor:

  • corePoolSize: The size of the core pool, this parameter has a great relationship with the implementation principle of the thread pool described later. After the thread pool is created, by default, there are no threads in the thread pool, but wait for a task to arrive before creating a thread to execute the task, unless the prestartAllCoreThreads() or prestartCoreThread() method is called, from these two methods As you can see from the name, it means pre-creating threads, that is, creating corePoolSize threads or a thread before no tasks arrive. By default, after the thread pool is created, the number of threads in the thread pool is 0. When a task arrives, a thread will be created to execute the task. When the number of threads in the thread pool reaches corePoolSize, it will be sent to The tasks are put into the cache queue;
  • maximumPoolSize: The maximum number of threads in the thread pool, this parameter is also a very important parameter, it indicates how many threads can be created in the thread pool at most;
  • keepAliveTime: Indicates how long the thread will be terminated when there is no task execution. By default, keepAliveTime will work only when the number of threads in the thread pool is greater than corePoolSize, until the number of threads in the thread pool is not greater than corePoolSize, that is, when the number of threads in the thread pool is greater than corePoolSize, if a thread is idle When the time reaches keepAliveTime, it will terminate until the number of threads in the thread pool does not exceed corePoolSize. However, if the allowCoreThreadTimeOut(boolean) method is called, when the number of threads in the thread pool is not greater than corePoolSize, the keepAliveTime parameter will also work until the number of threads in the thread pool is 0;
  • unit: The time unit of the parameter keepAliveTime, there are 7 values, and there are 7 static properties in the TimeUnit class:
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue: A blocking queue used to store tasks waiting to be executed. The choice of this parameter is also very important and will have a significant impact on the running process of the thread pool. Generally speaking, the blocking queue here has the following options:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

ArrayBlockingQueue and PriorityBlockingQueue are used less, and LinkedBlockingQueue and Synchronous are generally used. The queuing strategy of the thread pool is related to BlockingQueue.

  • threadFactory: thread factory, mainly used to create threads;
  • handler: Indicates the strategy when refusing to process a task, with the following four values:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

 The relationship between the configuration of specific parameters and the thread pool will be described in the next section.

From the code of the ThreadPoolExecutor class given above, we can know that ThreadPoolExecutor inherits AbstractExecutorService. Let's take a look at the implementation of AbstractExecutorService:

public abstract class AbstractExecutorService implements ExecutorService {
 
     
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
    public Future<?> submit(Runnable task) {};
    public <T> Future<T> submit(Runnable task, T result) { };
    public <T> Future<T> submit(Callable<T> task) { };
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                            boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
    };
}

AbstractExecutorService is an abstract class that implements the ExecutorService interface.

Let's look at the implementation of the ExecutorService interface:

public interface ExecutorService extends Executor {
 
    void shutdown();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

And ExecutorService inherits the Executor interface. Let's take a look at the implementation of the Executor interface:

public interface Executor {
    void execute(Runnable command);
}

At this point, you should understand the relationship between ThreadPoolExecutor, AbstractExecutorService, ExecutorService and Executor.

Executor is a top-level interface, in which only one method execute(Runnable) is declared, the return value is void, and the parameter is Runnable type. It can be understood from the literal meaning that it is used to execute the tasks passed in;

Then the ExecutorService interface inherits the Executor interface and declares some methods: submit, invokeAll, invokeAny, shutDown, etc.;

The abstract class AbstractExecutorService implements the ExecutorService interface and basically implements all the methods declared in ExecutorService;

Then ThreadPoolExecutor inherits the class AbstractExecutorService.

There are several very important methods in the ThreadPoolExecutor class:

execute()
submit()
shutdown()
shutdownNow()

The execute() method is actually a method declared in Executor, which is specifically implemented in ThreadPoolExecutor. This method is the core method of ThreadPoolExecutor. Through this method, a task can be submitted to the thread pool for execution by the thread pool.

The submit() method is a method declared in ExecutorService. It has been implemented in AbstractExecutorService. It is not rewritten in ThreadPoolExecutor. This method is also used to submit tasks to the thread pool, but it is different from execute() ) method is different, it can return the result of the task execution, look at the implementation of the submit() method, you will find that it actually calls the execute() method, but it uses the Future to obtain the task execution result (Future related content will be described in the next article).

shutdown() and shutdownNow() are used to shut down the thread pool.

There are many other ways:

For example: getQueue(), getPoolSize(), getActiveCount(), getCompletedTaskCount() and other methods to obtain properties related to thread pools. Interested friends can check the API by themselves.

Second, in-depth analysis of the principle of thread pool implementation

In the previous section, we introduced ThreadPoolExecutor from a macro perspective. Let's analyze the specific implementation principle of the thread pool in depth. We will explain it from the following aspects:

1. Thread pool status

A volatile variable is defined in ThreadPoolExecutor, and several static final variables are defined to represent the various states of the thread pool:

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState represents the state of the current thread pool, which is a volatile variable used to ensure visibility between threads;

The following static final variables represent several possible values ​​of runState.

When the thread pool is created, initially, the thread pool is in the RUNNING state;

If the shutdown() method is called, the thread pool is in the SHUTDOWN state. At this time, the thread pool cannot accept new tasks, and it will wait for all tasks to be executed;

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;

When the thread pool is in the SHUTDOWN or STOP state, and all worker threads have been destroyed, the task cache queue has been emptied or the execution has ended, the thread pool is set to the TERMINATED state.

2. Execution of tasks

Before understanding the whole process of submitting a task to the thread pool to the completion of the task execution, let's take a look at some other important member variables in the ThreadPoolExecutor class:

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小
                                                              //、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集
 
private volatile long  keepAliveTime;    //线程存货时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
 
private volatile int   poolSize;       //线程池中当前的线程数
 
private volatile RejectedExecutionHandler handler; //任务拒绝策略
 
private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
 
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
 
private long completedTaskCount;   //用来记录已经执行完毕的任务个数

The role of each variable has been marked out, here to focus on explaining the three variables corePoolSize, maximumPoolSize, largestPoolSize.

corePoolSize is translated into the core pool size in many places. In fact, my understanding is the size of the thread pool. Take a simple example:

If there is a factory, there are 10 workers in the factory, and each worker can only do one task at a time.

Therefore, as long as one of the 10 workers is idle, the task will be assigned to the idle worker;

When 10 workers have tasks to do, if there is still a task, the task will be queued and waited;

If the number of new tasks is growing much faster than the speed at which workers are doing tasks, then the factory supervisor may want to take remedial measures, such as recruiting 4 temporary workers;

Then the tasks are also assigned to these 4 temporary workers;

If the speed of 14 workers is still not enough, the factory supervisor may consider not accepting new tasks or abandoning some previous tasks.

When some of the 14 workers are free, and the growth rate of new tasks is relatively slow, the factory supervisor may consider quitting 4 temporary workers and only keep the original 10 workers. After all, it costs money to hire additional workers. of.

The corePoolSize in this example is 10, and the maximumPoolSize is 14 (10+4).

In other words, corePoolSize is the size of the thread pool, and maximumPoolSize is a remedy for the thread pool in my opinion, that is, a remedy when the amount of tasks suddenly becomes too large.

However, for the convenience of understanding, the corePoolSize is translated into the core pool size later in this article.

largestPoolSize is just a variable used for recording, which is used to record the largest number of threads that have ever existed in the thread pool, and has nothing to do with the capacity of the thread pool.

Now let's get to the point and see what process the task has gone through from submission to final execution.

In the ThreadPoolExecutor class, the core task submission method is the execute() method. Although tasks can also be submitted through submit, in fact, the execute() method is actually called in the submit method, so we only need to study the execute() method. The realization principle can be:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
}

The above code may not seem so easy to understand, let's explain it one by one:

First, determine whether the submitted task command is null. If it is null, a null pointer exception will be thrown;

Then there is this sentence, which should be well understood:

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

Because it is the OR conditional operator, the first half of the value is calculated first. If the current number of threads in the thread pool is not less than the core pool size, it will directly enter the following if statement block.

If the current number of threads in the thread pool is less than the size of the core pool, then execute the second half, that is, execute

addIfUnderCorePoolSize(command)

If the addIfUnderCorePoolSize method returns false after executing the method, continue to execute the following if statement block, otherwise the entire method will be executed directly.

If the addIfUnderCorePoolSize method returns false after executing, then judge:

if (runState == RUNNING && workQueue.offer(command))

If the current thread pool is in the RUNNING state, put the task into the task cache queue; if the current thread pool is not in the RUNNING state or the task fails to be put into the cache queue, execute:

addIfUnderMaximumPoolSize(command)

If executing the addIfUnderMaximumPoolSize method fails, execute the reject() method for task rejection.

Back to the front:

if (runState == RUNNING && workQueue.offer(command))

The execution of this sentence, if the current thread pool is in the RUNNING state and the task is successfully placed in the task cache queue, continue to judge:

if (runState != RUNNING || poolSize == 0)

This judgment is an emergency measure to prevent other threads from suddenly calling the shutdown or shutdownNow method to close the thread pool when the task is added to the task cache queue. If so execute:

ensureQueuedTaskHandled(command)

Emergency processing, as the name can tell, is to ensure that tasks added to the task cache queue are processed.

Let's look at the implementation of two key methods: addIfUnderCorePoolSize and addIfUnderMaximumPoolSize:

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //创建线程去执行firstTask任务   
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

This is the specific implementation of the addIfUnderCorePoolSize method. As can be seen from the name, its intention is to execute the method when it is lower than the core size. Let's look at its specific implementation. First, the lock is acquired, because this place involves the change of the thread pool state. First, the if statement is used to determine whether the number of threads in the current thread pool is less than the size of the core pool. Some friends may have doubts: before execute Hasn’t it been judged in the () method? The addIfUnderCorePoolSize method will only be executed if the current number of threads in the thread pool is less than the core pool size. Why do we continue to judge here? The reason is very simple. There is no lock in the previous judgment process. Therefore, the poolSize may be smaller than the corePoolSize when the execute method is judged. After the judgment, other threads submit tasks to the thread pool, which may cause the poolSize to not be smaller than the corePoolSize. , so you need to continue to judge in this place. Then it is judged whether the status of the thread pool is RUNNING. The reason is also very simple, because it is possible that the shutdown or shutdownNow method has been called in other threads. then execute

t = addThread(firstTask);

This method is also very critical. The parameter passed in is the submitted task, and the return value is of type Thread. Then judge whether t is empty or not. If it is empty, it indicates that the thread creation failed (that is, poolSize>=corePoolSize or runState is not equal to RUNNING), otherwise, the t.start() method is called to start the thread.

Let's take a look at the implementation of the addThread method:

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务   
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量       
        workers.add(w);
        int nt = ++poolSize;     //当前线程数加1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

In the addThread method, first create a Worker object with the submitted task, then call the thread factory threadFactory to create a new thread t, then assign the reference of thread t to the member variable thread of the Worker object, and then pass workers.add (w) Add the Worker object to the working set.

Let's take a look at the implementation 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);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
            //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等           
            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);   //当任务队列中没有任务时,进行清理工作       
        }
    }
}

It actually implements the Runnable interface, so the effect of the above Thread t = threadFactory.newThread(w); is basically the same as the effect of the following sentence:

Thread t = new Thread(w);

It is equivalent to passing in a Runnable task and executing this Runnable in thread t.

Since Worker implements the Runnable interface, the natural core method is the run() method:

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

It can be seen from the implementation of the run method that it first executes the task firstTask passed in through the constructor. After calling runTask() to execute the firstTask, it continues to fetch new tasks to execute through getTask() in the while loop. So where to get it? Naturally, it is taken from the task cache queue. getTask is a method in the ThreadPoolExecutor class, not a method in the Worker class. The following is the implementation of the getTask method:

Runnable getTask() {
    for (;;) {
        try {
            int state = runState;
            if (state > SHUTDOWN)
                return null;
            Runnable r;
            if (state == SHUTDOWN)  // Help drain queue
                r = workQueue.poll();
            else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
                //则通过poll取任务,若等待一定的时间取不到任务,则返回null
                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            else
                r = workQueue.take();
            if (r != null)
                return r;
            if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出
                if (runState >= SHUTDOWN) // Wake up others
                    interruptIdleWorkers();   //中断处于空闲状态的worker
                return null;
            }
            // Else retry
        } catch (InterruptedException ie) {
            // On interruption, re-check runState
        }
    }
}

In getTask, first judge the current thread pool state, and if runState is greater than SHUTDOWN (ie, STOP or TERMINATED), return null directly.

If runState is SHUTDOWN or RUNNING, the task is fetched from the task cache queue.

If the number of threads in the current thread pool is greater than the core pool size corePoolSize or the idle survival time is allowed to be set for the threads in the core pool, call poll(time, timeUnit) to fetch the task, this method will wait for a certain time, if the task cannot be fetched returns null.

Then judge whether the fetched task r is null, and if it is null, judge whether the current worker can exit by calling the workerCanExit() method. Let's take a look at the implementation of workerCanExit():

private boolean workerCanExit() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    boolean canExit;
    //如果runState大于等于STOP,或者任务缓存队列为空了
    //或者  允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1
    try {
        canExit = runState >= STOP ||
            workQueue.isEmpty() ||
            (allowCoreThreadTimeOut &&
             poolSize > Math.max(1, corePoolSize));
    } finally {
        mainLock.unlock();
    }
    return canExit;
}

That is to say, if the thread pool is in the STOP state, or the task queue is empty, or the idle survival time is allowed to be set for the core pool thread and the number of threads is greater than 1, the worker is allowed to exit. If the worker is allowed to exit, call interruptIdleWorkers() to interrupt the idle worker. Let's take a look at the implementation of interruptIdleWorkers():

void interruptIdleWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)  //实际上调用的是worker的interruptIfIdle()方法
            w.interruptIfIdle();
    } finally {
        mainLock.unlock();
    }
}

As can be seen from the implementation, it actually calls the worker's interruptIfIdle() method, in the worker's interruptIfIdle() method:

void interruptIfIdle() {
    final ReentrantLock runLock = this.runLock;
    if (runLock.tryLock()) {    //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的
                                //如果成功获取了锁,说明当前worker处于空闲状态
        try {
    if (thread != Thread.currentThread())  
    thread.interrupt();
        } finally {
            runLock.unlock();
        }
    }
}

There is a very ingenious design method here. If we design the thread pool, there may be a task dispatching thread. When a thread is found to be idle, a task will be taken from the task cache queue and given to the idle thread for execution. However, this method is not adopted here, because it will additionally manage the task dispatching thread, which will invisibly increase the difficulty and complexity. Here, the thread that has executed the task is directly asked to go to the task cache queue to fetch the task for execution. .

Let's look at the implementation of the addIfUnderMaximumPoolSize method. The implementation idea of ​​this method is very similar to that of the addIfUnderCorePoolSize method. The only difference is that the addIfUnderMaximumPoolSize method is that the number of threads in the thread pool reaches the core pool size and adding tasks to the task queue fails. Executed in case of:

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;
}

See no, in fact, it is basically the same as the implementation of the addIfUnderCorePoolSize method, except that the poolSize < maximumPoolSize in the if statement judgment condition is different.

At this point, most of my friends should have a basic understanding of the entire process from the time the task is submitted to the thread pool to the execution. The following summarizes:

1) First, be clear about the meaning of corePoolSize and maximumPoolSize;

2) Second, you need to know what role Worker is used for;

3) To know the processing strategy after the task is submitted to the thread pool, here are four main points:

  • If the number of threads in the current thread pool is less than corePoolSize, each time a task comes, a thread will be created to execute the task;
  • If the number of threads in the current thread pool is >= corePoolSize, each time a task comes, it will try to add it to the task cache queue. If the addition is successful, the task will wait for an idle thread to take it out for execution; if the addition fails ( Generally speaking, the task cache queue is full), it will try to create a new thread to execute the task;
  • If the number of threads in the current thread pool reaches the maximumPoolSize, the task rejection policy will be adopted for processing;
  • If the number of threads in the thread pool is greater than corePoolSize, if the idle time of a thread 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, then the core pool The thread in the idle time exceeds keepAliveTime, the thread will also be terminated.

3. Thread initialization in the thread pool

By default, after the thread pool is created, there are no threads in the thread pool, and threads are created only after tasks are submitted.

In practice, if you need to create a thread immediately after the thread pool is created, you can do it in the following two ways:

  • prestartCoreThread(): Initialize a core thread;
  • prestartAllCoreThreads(): Initialize all core threads

The following is the implementation of these two methods:

public boolean prestartCoreThread() {
    return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
 
public int prestartAllCoreThreads() {
    int n = 0;
    while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
        ++n;
    return n;
}

Note that the parameter passed in above is null. According to the analysis in Section 2, if the parameter passed in is null, the final execution thread will be blocked in the getTask method.

r = workQueue.take();

That is, waiting for a task in the task queue.

4. Task cache queue and queuing strategy

We have mentioned the task cache queue many times before, that is, workQueue, which is used to store tasks waiting to be executed.

The type of workQueue is BlockingQueue<Runnable>, which can usually take the following three types:

1) ArrayBlockingQueue: an array-based first-in-first-out queue, the size must be specified when the queue is created;

2) LinkedBlockingQueue: A first-in, first-out queue based on a linked list. If the queue size is not specified when it is created, the default value is Integer.MAX_VALUE;

3) synchronousQueue: This queue is special, it will not save the submitted tasks, but will directly create a new thread to execute the new task.

5. Task Rejection Policy

When the task cache queue of the thread pool is full and the number of threads in the thread pool reaches the maximumPoolSize, if there are still tasks coming, the task rejection strategy will be adopted. There are usually the following four strategies:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6. Closing the thread pool

ThreadPoolExecutor provides two methods for thread pool shutdown, shutdown() and shutdownNow(), where:

  • shutdown(): The thread pool will not be terminated immediately, but will be terminated after all tasks in the task cache queue have been executed, but no new tasks will be accepted.
  • shutdownNow(): Immediately terminate the thread pool, try to interrupt the task being executed, and clear the task cache queue and return the task that has not been executed

7. Dynamic adjustment of thread pool capacity

ThreadPoolExecutor provides methods to dynamically adjust the size of the thread pool: setCorePoolSize() and setMaximumPoolSize(),

  • setCorePoolSize: Set the core pool size
  • setMaximumPoolSize: Set the maximum number of threads that the thread pool can create

When the above parameters change from small to large, ThreadPoolExecutor performs thread assignment and may immediately create new threads to execute tasks.

3. Example of use

Earlier we discussed the implementation principle of the thread pool. In this section, let's take a look at its specific use:

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){
             MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
             executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}
 
 
class MyTask implements Runnable {
    private int taskNum;
     
    public MyTask(int num) {
        this.taskNum = num;
    }
     
    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕");
    }
}

Results of the:

正在执行task 0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 14
task 3执行完毕
task 0执行完毕
task 2执行完毕
task 1执行完毕
正在执行task 8
正在执行task 7
正在执行task 6
正在执行task 5
task 4执行完毕
task 10执行完毕
task 11执行完毕
task 13执行完毕
task 12执行完毕
正在执行task 9
task 14执行完毕
task 8执行完毕
task 5执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕

It can be seen from the execution results that when the number of threads in the thread pool is greater than 5, the task is put into the task cache queue, and when the task cache queue is full, a new thread is created. If in the above program, the for loop is changed to execute 20 tasks, a task rejection exception will be thrown.

However, in the java doc, we do not recommend that we use ThreadPoolExecutor directly, but use several static methods provided in the Executors class to create a thread pool:

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池
Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

The following is the specific implementation of these three static methods:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

From their specific implementation, they actually call ThreadPoolExecutor, but the parameters have been configured.

The thread pool corePoolSize and maximumPoolSize values ​​created by newFixedThreadPool are equal, and it uses LinkedBlockingQueue;

newSingleThreadExecutor sets both corePoolSize and maximumPoolSize to 1, and also uses LinkedBlockingQueue;

newCachedThreadPool sets corePoolSize to 0, maximumPoolSize to Integer.MAX_VALUE, and uses SynchronousQueue, which means that when a task comes, a thread is created to run, and when the thread is idle for more than 60 seconds, the thread is destroyed.

In practice, if the three static methods provided by Executors can meet the requirements, try to use the three methods provided by it, because it is a bit troublesome to manually configure the parameters of ThreadPoolExecutor by yourself, and it needs to be configured according to the type and number of actual tasks.

In addition, if ThreadPoolExecutor does not meet the requirements, you can inherit the ThreadPoolExecutor class and rewrite it.

Fourth, how to reasonably configure the size of the thread pool

This section discusses a more important topic: how to reasonably configure the size of the thread pool, for reference only.

Generally, the thread pool size needs to be configured according to the type of task:

If it is a CPU-intensive task, you need to squeeze the CPU as much as possible. The reference value can be set to  N CPU+1

If it is an IO-intensive task, the reference value can be set to 2* N CPU

Of course, this is only a reference value, and the specific settings need to be adjusted according to the actual situation. For example, you can first set the thread pool size to the reference value, and then observe the task operation, system load, and resource utilization to make appropriate adjustments.

Guess you like

Origin blog.csdn.net/qq_41701956/article/details/123488240