Thread pool ThreadPoolExecutor underlying principle source code analysis

What is the specific process of thread pool execution tasks?

ThreadPoolExecutor provides two methods for executing tasks:

  • void execute(Runnable command)
  • Future<?> submit(Runnable task)

In fact, the execute() method is ultimately called in submit, but it returns a Future object to obtain the task execution result:

public Future<?> submit(Runnable task) {
    
    
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

The execute(Runnable command) method will be executed in three steps:
Insert image description here

Notice:

  • When submitting a Runnable, regardless of whether the threads in the current thread pool are idle or not, new threads will be created as long as the number is less than the number of core threads.
  • ThreadPoolExecutor is equivalent to unfairness. For example, a Runnable submitted after the queue is full may be executed before the Runnable being queued.

How do the five states of the thread pool flow?

The thread pool has five states

  • RUNNING: New tasks will be received and tasks in the queue will be processed
  • SHUTDOWN: New tasks will not be accepted and tasks in the queue will be processed
  • STOP: New tasks will not be received and tasks in the queue will not be processed, and tasks being processed will be interrupted (note: whether a task can be interrupted depends on the task itself)
  • TIDYING: All tasks have been terminated and there are no threads in the thread pool, so the status of the thread pool will change to TIDYING. Once this status is reached, terminated() of the thread pool will be called.
  • TERMINATED: After terminated() is executed, it will change to TERMINATED

These five states cannot be converted arbitrarily, and there will only be the following conversion situations:

  • RUNNING -> SHUTDOWN: Triggered by manually calling shutdown(), or finalize() will be called during thread pool object GC to call shutdown()
  • (RUNNING or SHUTDOWN) -> STOP: Triggered by calling shutdownNow(). If shutdown() is called first and shutdownNow() is called immediately, SHUTDOWN -> STOP will occur.
  • SHUTDOWN -> TIDYING: automatic conversion when the queue is empty and there are no threads in the thread pool
  • STOP -> TIDYING: Automatically switch when there are no threads in the thread pool (there may be tasks in the queue)
  • TIDYING -> TERMINATED: It will be automatically converted after terminated() is executed.

How threads in the thread pool are closed

  • We usually use the thread.start() method to start a thread, but how to stop a thread?
  • The Thread class provides a stop(), but it is marked @Deprecated. Why is it not recommended to use the stop() method to stop the thread?
  • Because the stop() method is too crude, once stop() is called, the thread will be stopped directly. However, when calling, you have no idea what the thread was just doing or what step the task has reached. This is very dangerous.
  • It is emphasized here that stop() will release the synchronized lock occupied by the thread (the ReentrantLock lock will not be automatically released, which is also a factor why stop() is not recommended ).
public class ThreadTest {
    
    
    static int count = 0;
    static final Object lock = new Object();
    static final ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    
//                synchronized (lock) {
    
    
                reentrantLock.lock();
                for (int i = 0; i < 100; i++) {
    
    
                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
//                }
                reentrantLock.unlock();
            }
        });

        thread.start();
        Thread.sleep(5*1000);
        thread.stop();
//
//        Thread.sleep(5*1000);

        reentrantLock.lock();
        System.out.println(count);
        reentrantLock.unlock();

//        synchronized (lock) {
    
    
//            System.out.println(count);
//        }
    }
}

Therefore, we recommend stopping a thread by customizing a variable or interrupting, such as:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (stop) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        stop = true;
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

The difference is that when we set stop to true, the thread itself can control whether and when to stop. Similarly, we can call interrupt() of thread to interrupt the thread:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (Thread.currentThread().isInterrupted()) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        break;
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        thread.interrupt();
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

The difference is that if the thread is interrupted during sleep, an exception will be received.
In fact, interrupt() is used to stop threads in the thread pool . For example, the shutdownNow() method will call

void interruptIfStarted() {
    
    
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
        try {
    
    
            t.interrupt();
        } catch (SecurityException ignore) {
    
    
        }
    }
}

Why does the thread pool have to be a blocking queue?

  During the running process, the threads in the thread pool will continue to obtain tasks from the queue and execute them after executing the first task bound when creating the thread. Then if there are no tasks in the queue, the thread will not die naturally. It will be blocked when acquiring the queue task, and when there is a task in the queue, it will get the task and execute the task.
  This method can ultimately ensure that a specified number of core threads can be reserved in the thread pool. The key code is:

try {
    
    
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    
    
    timedOut = false;
}

  When a thread obtains a task from the queue, it will determine whether to use timeout blocking to obtain it. We can think that the non-core thread will poll(), the core thread will take(), and the non-core thread will naturally fail to obtain the task after the time expires. Died.

If an exception occurs to a thread, will it be removed from the thread pool?

The answer is yes, then is it possible that the number of core threads is wrong when executing tasks, causing all core threads to be removed from the thread pool?
Insert image description here

  • In the source code, when an exception occurs when executing a task, processWorkerExit() will eventually be executed. After executing this method, the current thread will die naturally.
  • However, an additional thread will be added in the processWorkerExit() method, so that the fixed number of core threads can be maintained.

How Tomcat customizes the thread pool

The thread pool used in Tomcat is org.apache.tomcat.util.threads.ThreadPoolExecutor. Note that the class name is the same as that under JUC, but the package name is different.
Tomcat will create this thread pool:

public void createExecutor() {
    
    
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

The incoming queue is TaskQueue, and its enqueuing logic is:

public boolean offer(Runnable o) {
    
    
    //we can't do any checks
    if (parent==null) {
    
    
        return super.offer(o);
    }

    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
    
    
        return super.offer(o);
    }

    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
    
    
        return super.offer(o);
    }

    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
    
    
        return false;
    }

    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

Special in:

  • When joining the queue, only join the queue if the number of threads in the thread pool is equal to the maximum number of thread pools
  • When joining the queue, if the number of threads in the thread pool is less than the maximum number of thread pools, false will be returned, indicating that joining the queue failed.

This controls the thread pool of Tomcat when submitting a task:

  • It will still first determine whether the number of threads is less than the number of core threads. If it is less than the number of core threads, create a thread.
  • If it is equal to the number of core threads, it will join the queue. However, if the number of threads is less than the maximum number of threads, joining the queue will fail and threads will be created.

Therefore, as the task is submitted, threads will be created first and will not join the queue until the number of threads equals the maximum number of threads.

Of course, there is a relatively detailed logic: when submitting a task, if the number of tasks being processed is less than the number of threads in the thread pool, it will be directly added to the queue without creating a thread, which is the getSubmittedCount in the source code above. role.

How to set the number of core threads and the maximum number of threads in the thread pool

There are two very important parameters in the thread pool:

  • corePoolSize: The number of core threads, indicating the number of resident threads in the thread pool
  • maximumPoolSize: the maximum number of threads, indicating the maximum number of threads that can be opened in the thread pool

So how to set these two parameters?

The tasks we are responsible for executing by the thread pool are divided into three situations:

  • CPU-intensive tasks, such as finding prime numbers from 1 to 1,000,000
  • IO-intensive tasks, such as file IO and network IO
  • mixed tasks

  The characteristics of CPU-intensive tasks are that threads will always utilize the CPU when executing tasks, so in this case, thread context switching should be avoided as much as possible.
  For example, my computer now only has one CPU. If two threads are executing the task of finding prime numbers at the same time, then the CPU needs to perform additional thread context switching to achieve the effect of thread parallelism. At this time, the two threads are executing The total time is:
  task execution time 2 + thread context switching time
  . If there is only one thread and this thread performs two tasks, then the time is:
  task execution time
2.
  So for CPU-intensive tasks, the number of threads is best equal to the CPU Core number, you can get the core number of your computer through the following API:

Runtime.getRuntime().availableProcessors()

However, in order to respond to thread blocking requests caused by page faults or other exceptions during thread execution, we can set up an additional thread, so that when a thread temporarily does not need the CPU, there can be a substitute thread to continue to utilize the CPU.

Therefore, for CPU-intensive tasks, we can set the number of threads to: number of CPU cores + 1

Let's look at IO-type tasks. When threads perform IO-type tasks, they may be blocked on IO most of the time. If there are 10 CPUs now, if we only set up 10 threads to perform IO-type tasks, then it will be very difficult. Maybe these 10 threads are blocked on IO, so these 10 CPUs have no work to do. Therefore, for IO tasks, we usually set the number of threads to: 2*number of CPU cores.

However, even if it is set to 2*CPU core number, it may not be optimal. For example, if there are 10 CPUs and the number of threads is 20, then it is possible that these 20 threads are blocked on IO at the same time, so you can add more Threads, thereby squeezing CPU utilization.

Usually, if the execution time of IO-type tasks is longer, then there may be more threads blocked on IO at the same time, and we can set up more threads. However, more threads are definitely not better. We can use the following
formula To calculate:
Number of threads = Number of CPU cores * (1 + thread waiting time / total thread running time)

  • Thread waiting time: refers to the time when the thread is not using the CPU, such as blocking in IO
  • Total thread running time: refers to the total time it takes for a thread to complete a task

Source code analysis of basic properties and methods in thread pool

In the source code of the thread pool, a variable ctl of type AtomicInteger is used to represent the status of the thread pool and the number of working threads in the current thread pool.
An Integer occupies 4 bytes, which is 32 bits. The thread pool has 5 states:

  • RUNNING: The thread pool is running normally and can accept and process tasks normally.
  • SHUTDOWN: The thread pool is closed and cannot accept new tasks. However, the thread pool will execute the remaining tasks in the blocking queue. After the remaining tasks are processed, all worker threads will be interrupted.
  • STOP: The thread pool has stopped, cannot accept new tasks, and will not process tasks in the blocking queue, interrupting all worker threads.
  • TIDYING: After all the working threads in the current thread pool are stopped, TIDYING will be entered.
  • TERMINATED: After the thread pool is in the TIDYING state, it will execute the terminated() method. After execution, it will enter the TERMINATED state. In ThreadPoolExecutor, terminated() is an empty method. You can customize the thread pool to override this method.

2 bits can represent 4 states, and those 5 states require at least three bits. For example, this is how it is represented in the source code of the thread pool:

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

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

Integer.SIZE is 32, so COUNT_BITS is 29. The final secondary system corresponding to each state is:

  • RUNNING:11100000 00000000 00000000 00000000
  • SHUTDOWN:00000000 00000000 00000000 00000000
  • STOP:00100000 00000000 00000000 00000000
  • TIDYING:01000000 00000000 00000000 00000000
  • TERMINATED:01100000 00000000 00000000 00000000

Therefore, you only need to use the highest three bits of an Integer number to represent the status of 5 thread pools, and the remaining 29 bits can be used to represent the number of working threads . For example, if the ctl is: 11100000 00000000 00000000 00001010 , it means that the status of the thread pool is RUNNING, and there are 10 threads currently working in the thread pool. The "working" here means that the thread is alive, either executing tasks or blocking and waiting for tasks.

At the same time, the thread pool also provides some methods to obtain the thread pool status and the number of working threads, such as:

// 29,二进制为00000000 00000000 00000000 00011101
private static final int COUNT_BITS = Integer.SIZE - 3;

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

// ~CAPACITY为11100000 00000000 00000000 00000000
// &操作之后,得到就是c的高3位
private static int runStateOf(int c)     {
    
     
    return c & ~CAPACITY; 
}

// CAPACITY为00011111 11111111 11111111 11111111
// &操作之后,得到的就是c的低29位
private static int workerCountOf(int c)  {
    
     
    return c & CAPACITY; 
}

At the same time, there is another method:

private static int ctlOf(int rs, int wc) {
    
     
    return rs | wc; 
}

It is a method used to combine the running status and the number of worker threads. However, the two int numbers passed into this method are limited. The lower 29 bits of rs must be 0, and the upper 3 bits of wc must be 0, so After the OR operation, the accurate ctl can be obtained.

At the same time, there are some related methods

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// c状态是否小于s状态,比如RUNNING小于SHUTDOWN
private static boolean runStateLessThan(int c, int s) {
    
    
    return c < s;
}

// c状态是否大于等于s状态,比如STOP大于SHUTDOWN
private static boolean runStateAtLeast(int c, int s) {
    
    
    return c >= s;
}

// c状态是不是RUNNING,只有RUNNING是小于SHUTDOWN的
private static boolean isRunning(int c) {
    
    
    return c < SHUTDOWN;
}

// 通过cas来增加工作线程数量,直接对ctl进行加1
// 这个方法没考虑是否超过最大工作线程数的(2的29次方)限制,源码中在调用该方法之前会进行判断的
private boolean compareAndIncrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect + 1);
}

// 通过cas来减少工作线程数量,直接对ctl进行减1
private boolean compareAndDecrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect - 1);
}

execute method

When executing the execute method of the thread pool:

public void execute(Runnable command) {
    
    
    
    if (command == null)
        throw new NullPointerException();
    
    // 获取ctl
    // ctl初始值是ctlOf(RUNNING, 0),表示线程池处于运行中,工作线程数为0
    int c = ctl.get();
    
    // 工作线程数小于corePoolSize,则添加工作线程,并把command作为该线程要执行的任务
    if (workerCountOf(c) < corePoolSize) {
    
    
        // true表示添加的是核心工作线程,具体一点就是,在addWorker内部会判断当前工作线程数是不是超过了corePoolSize
        // 如果超过了则会添加失败,addWorker返回false,表示不能直接开启新的线程来执行任务,而是应该先入队
        if (addWorker(command, true))
            return;
        
        // 如果添加核心工作线程失败,那就重新获取ctl,可能是线程池状态被其他线程修改了
        // 也可能是其他线程也在向线程池提交任务,导致核心工作线程已经超过了corePoolSize
        c = ctl.get();
    }
    
    // 线程池状态是否还是RUNNING,如果是就把任务添加到阻塞队列中
    if (isRunning(c) && workQueue.offer(command)) {
    
    
        
        // 在任务入队时,线程池的状态可能也会发生改变
        // 再次检查线程池的状态,如果线程池不是RUNNING了,那就不能再接受任务了,就得把任务从队列中移除,并进行拒绝策略
        
        // 如果线程池的状态没有发生改变,仍然是RUNNING,那就不需要把任务从队列中移除掉
        // 不过,为了确保刚刚入队的任务有线程会去处理它,需要判断一下工作线程数,如果为0,那就添加一个非核心的工作线程
        // 添加的这个线程没有自己的任务,目的就是从队列中获取任务来执行
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果线程池状态不是RUNNING,或者线程池状态是RUNNING但是队列满了,则去添加一个非核心工作线程
    // 实际上,addWorker中会判断线程池状态如果不是RUNNING,是不会添加工作线程的
    // false表示非核心工作线程,作用是,在addWorker内部会判断当前工作线程数已经超过了maximumPoolSize,如果超过了则会添加不成功,执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

addWorker method

The addWorker method is the core method and is used to add threads. The core parameter indicates whether to add a core thread or a non-core thread.
Before looking at this method, we might as well analyze it ourselves first, what is adding a thread?
In fact, you need to start a thread. Whether it is a core thread or a non-core thread, it is actually just an ordinary thread. The difference between core and non-core is: if you want to
add a core worker thread, you have to determine the current number of worker threads. Whether it exceeds corePoolSize.
If it does not, a new worker thread will be started directly to perform the task.
If it is exceeded, a new worker thread will not be started, but the task will be queued.
If you want to add a non-core worker thread, you must judge the current situation. Whether the number of working threads exceeds maximumPoolSize.
If not, a new working thread will be started to perform the task directly.
If it is exceeded, the task will be refused to be executed.
Therefore, in the addWorker method, it is first necessary to determine whether the working thread has exceeded the limit. If it does not exceed the limit, then To start a thread.

And in the addWorker method, you have to determine the status of the thread pool. If the status of the thread pool is not RUNNING, then there is no need to add threads. Of course, there is a special case, that is, the status of the thread pool is SHUTDOWN, but in the queue If there is a task, you still need to add a thread at this time.

So how did this special case arise?

What we mentioned earlier is to start a new working thread, so how to recycle the working thread? It is impossible for the opened working thread to always be alive, because if the number of tasks decreases from more to fewer, there will be no need for too many thread resources, so there will be a mechanism in the thread pool to recycle the started working thread. How to recycle it will be discussed later. As mentioned, let’s first analyze whether it is possible that all threads in the thread pool have been recycled. The answer is yes.

First of all, it is understandable that non-core worker threads are recycled, but should core worker threads be recycled? In fact, the meaning of the existence of the thread pool is to generate thread resources in advance, and use them directly when a thread is needed. There is no need to temporarily open the thread. Therefore, under normal circumstances, the opened core worker thread does not need to be recycled, even if it is not available temporarily. If the task needs to be processed, there is no need to recycle it. Just let the core worker thread wait there.

but! There is such a parameter in the thread pool: allowCoreThreadTimeOut, which indicates whether the core worker thread is allowed to timeout, which means whether the core worker thread is allowed to be recycled. The default parameter is false, but we can call allowCoreThreadTimeOut(boolean value) to change this parameter to true. , as long as it is changed, the core worker thread will also be recycled, so all the worker threads in the thread pool may be recycled. Then if all the worker threads are recycled, a task comes in the blocking queue, This creates special cases.

Guess you like

Origin blog.csdn.net/beautybug1126/article/details/132072664