Thread pool principle

Keywords: Java Concurrent Programming

Thread pool

If too many threads are created in the JVM, the system may run out of resources due to excessive memory consumption or "excessive switching".

In order to solve this problem, the first thought is pooling, so there is the concept of thread pool.

The core logic of the thread pool is to create several threads in advance and put them in a container. If a task needs to be processed, the task will be directly assigned to the thread in the thread pool for execution. After the task is processed, the thread will not be destroyed, but wait for subsequent task allocation, so as to reduce the overhead caused by thread creation and destruction.

characteristic

Reasonable use of thread pool can bring some benefits

  • Reduce the performance overhead of creating and destroying threads
  • Improve the response speed. When a new task needs to be executed, it can be executed immediately without waiting for the thread to be created
  • Reasonably setting the thread pool pair size can avoid the problem caused by the number of threads exceeding the hardware resource bottleneck

use

Java has provided several different thread pool pairs for the JUC package. Using the provided API can quickly meet the daily development needs.

Example

/**
* Create a thread pool with a fixed number of threads of 5 and execute 10 tasks
*/
public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new TestTask());
        }
        threadPool.shutdown();
    }

    static class TestTask implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is running");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

API

Executors

JUC provides a tool class java.util.concurrent.Executors, which provides many methods to create thread pool and thread factory objects.

There are four common strategies for thread pooling:

  • newFixedThreadPool
  • newSingleThreadExecutor
  • newCachedThreadPool
  • newScheduledThreadPool

newFixedThreadPool

A thread pool with a fixed number of threads. The number of threads remains unchanged. When a task is submitted to the thread pool, if there are idle threads, it will be executed immediately. If there are no idle threads, it will be added to the waiting queue.

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor

The thread pool with a fixed number of threads is 1. If the thread is idle, the task will be executed, otherwise the task will be added to the waiting queue.

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newCachedThreadPool

One can adjust the number of threads according to the actual situation. For the thread pool, the maximum number of threads is not limited. If there are idle threads, the task will be executed, and each idle thread will be automatically recycled after 60 seconds; If there are no tasks, no threads are created.

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

newScheduledThreadPool

A thread pool with latency and periodic execution tasks.

 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

ThreadPoolExecutor

The above four thread pools are implemented based on java.util.concurrent.ThreadPoolExecutor. The difference is that the construction parameter pairs are different.

There are 7 input parameters in the construction method, and the thread pool operation strategy is configured according to these input parameters.

public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                           RejectedExecutionHandler handler)
  • corePoolSize is the number of core threads. The number of threads held in the thread pool. Unless the allowCoreThreadTimeOut property is set, idle core threads will always be waiting
  • maximumPoolSize maximum number of threads, the maximum number of threads allowed in the pool
  • keepAliveTime survival time. When the number of threads in the pool is greater than the core capacity, the survival time of redundant idle threads will not exceed this value
  • Unit time unit
  • workQueue work queue is used to store unexecuted tasks. The task type can only be Runnable
    • For example, newScheduledThreadPool implements delayed tasks through a delay queue
  • The threadFactory thread project is used to create new threads
  • handler rejects the case when the processor is used to handle the maximum capacity of threads and queues

ThreadPoolExecutor is the core of thread pool and provides the implementation of thread pool. Based on ThreadPoolExecutor, we can implement thread pool according to customization.

principle

When an asynchronous task is submitted to the thread pool, how does the thread pool handle it?

The following figure shows this process

Submit task

The first step is to submit a task to the thread pool, that is, execute the java.util.concurrent.ThreadPoolExecutor#execute method. The ThreadPoolExecutor judges the processing method of the task according to the configured parameters such as the number of core threads, queue and maximum threads.

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        //1. If the number of currently running threads is less than the number of core threads, create a new thread to execute the task
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2. If the core pool is full, try to add the task to the queue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //3. Failed to join the queue, indicating that the queue is full. Try to create a new thread
    else if (!addWorker(command, false))
        //4. If it still fails, enable the reject policy
        reject(command);
}

Thread pool status

In the execute method, you can see that a variable ctl has a high exposure rate. It runs through the whole life cycle of the thread pool. Its main function is to save the number of threads and the state of the thread pool.

The following code is the variables and functions used to represent the thread pool state in the TreadPoolExecutor.

//It is used to save the status and number of threads of the thread pool. The high three bits represent the status of the thread pool and the low 29 bits represent the number of threads
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
//The status of thread pool is represented by the top three bits of ctl
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;
// Judge the thread pool status and the number of threads according to the ctl value
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
private static int workerCountOf(int c)  { return c & COUNT_MASK; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

private static boolean runStateLessThan(int c, int s) {
    return c < s;
}
private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}
private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}
  • RUNNING status, accept new tasks and execute tasks in the queue
  • SHUTDOWN does not accept new tasks and executes tasks in the queue
  • STOP does not accept new tasks, does not execute tasks in the queue, and interrupts the executing thread
  • TIDYING all tasks have ended, the number of threads is 0, and the terminate() method will be called
  • TERMINATED terminate() finished executing

Create a new thread

When the number of working threads in the thread pool is less than the number of core threads, a new thread will be created to process the task, and the addWorker method will be called.

The addWorker method mainly does two things:

1. Increase the number of working threads by one;

for (int c = ctl.get();;) {
    //Only when the thread pool is in running state or SHUTDOWN state and the queue is not empty can a new thread be created. Otherwise, false is returned directly
    if (runStateAtLeast(c, SHUTDOWN)
        && (runStateAtLeast(c, STOP)
            || firstTask != null
            || workQueue.isEmpty()))
        return false;

    for (;;) { //spin
        //If the number of working threads is greater than the default maximum number, false is returned directly
        if (workerCountOf(c)
            >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
            return false;
        if (compareAndIncrementWorkerCount(c)) //Try cas to increase the number of working threads by one. If it fails, continue the loop. If it succeeds, jump out of the loop and continue to create threads
            break retry;
        c = ctl.get();  // Re-read ctl
        if (runStateAtLeast(c, SHUTDOWN))
            continue retry;
    }
}

2. Create a new thread and enable it.

Worker w = null;
try {
    //Build a woker
    w = new Worker(firstTask);
    final Thread t = w.thread;
    if (t != null) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            int c = ctl.get();
            if (isRunning(c) ||
                (runStateLessThan(c, STOP) && firstTask == null)) {
                if (t.getState() != Thread.State.NEW)
                    throw new IllegalThreadStateException();
           //Only when the thread pool is in running state or SHUTDOWN state and the incoming task is empty can it be added to the wokers collection
                workers.add(w);
                workerAdded = true;
                int s = workers.size();
                if (s > largestPoolSize)
                    largestPoolSize = s;
            }
        } finally {
            mainLock.unlock();
        }
        if (workerAdded) {
            t.start();
            workerStarted = true;
        }
    }
} finally {
    if (! workerStarted)
        //If the addition fails, the number of threads added before will be reduced back
        addWorkerFailed(w);
}

woker

The thread created by the thread pool is a woker object.

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
        ... 
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
   		...
}

Woker inherits AQS and implements Runnable, so it is a thread and implements the function of exclusive lock, which is to ensure that the thread executing the task cannot be interrupted.

In the construction method, threads are created using the factory configured by the thread pool.

Perform tasks

The entry of the running task of the thread pool is the execute method. The core is to create the thread method addWoker, and the runWoker method in Woker actually executes the task.

After a woker is created, until it is destroyed by the thread pool, it is in the loop, constantly trying to get tasks from the queue for execution.

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    //woker can be interrupted when it does not execute a task, so it needs to release the exclusive lock
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //woker threads constantly try to obtain tasks, so as to realize thread reuse
        while (task != null || (task = getTask()) != null) {
            //woker in operation is not allowed to be interrupted
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //The pre operation is not implemented by default. If necessary, you can inherit and implement it yourself
                beforeExecute(wt, task);
                try {
                    //Perform tasks
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //Destroy threads without tasks
        processWorkerExit(w, completedAbruptly);
    }
}

Idle thread recycling

After the tasks submitted to the queue in the thread pool are executed, naturally there will be some threads, some of which will be destroyed, and only a few core threads will be retained (if allowCoreThreadTimeOut is true, the core threads will also be destroyed), and the idle judgment is realized in getTask.

 private Runnable getTask() {
     boolean timedOut = false; // Did the last poll() time out?
     for (;;) {
         int c = ctl.get();
         if (runStateAtLeast(c, SHUTDOWN)
             && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
             decrementWorkerCount();
             return null;
         }
         int wc = workerCountOf(c);
         boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
         if ((wc > maximumPoolSize || (timed && timedOut))
             && (wc > 1 || workQueue.isEmpty())) {
             //After the timeout, the number of worker threads is reduced by one and then destroyed
             if (compareAndDecrementWorkerCount(c))
                 return null;
             continue;
         }		
         try {
            //When woker obtains a task, a timeout time is added, that is, keepAliveTime. If it is not obtained, it is considered idle
             Runnable r = timed ?
                 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
             workQueue.take();
             if (r != null)
                 return r;
             //Idle threads will be destroyed on the next spin
             timedOut = true;
         } catch (InterruptedException retry) {
             timedOut = false;
         }
     }
 }

The implementation of idle thread recycling is to add a timeout time to the poll method of the queue, that is, keepAliveTime. If the thread whose task has not been obtained after the timeout is considered idle, it will be destroyed at the end of the runWoker method.

Reject policy

When a new task cannot be received in the thread pool, the java.util.concurrent.RejectedExecutionHandler#rejectedExecution() method is executed.

In JUC, there are four implementations of reject policy processors:

  • AbortPolicy throws exceptions directly. It is the default rejection policy
  • DiscardPolicy discards tasks directly, but does not throw exceptions
  • DiscardOldestPolicy discards the first task waiting in the queue and calls the execute() method again
  • CallerRunsPolicy executes the task directly with the thread of the caller

In addition to the 4 rejection policies provided, they can also be customized by implementing the RejectedExecutionHandler interface.

summary

In Alibaba Java development manual, there is a provision on thread pool

In our daily development work, we often use thread pool. Therefore, after understanding the principle of thread pool, we should try our best to configure the parameters of thread pool according to the actual scene.

Rational allocation

How to set the thread pool size is reasonable?

CPU intensive

If it is CPU intensive and mainly performs computing tasks with fast response time, and the CPU utilization of such tasks is very high, the number of threads should be set according to the number of CPU cores.

Too many threads will lead to context switching, which will reduce the efficiency. Then the maximum number of threads in the thread pool can be configured as the number of CPU cores + 1.

IO intensive

If it is Io intensive, it is mainly for IO operations. If the execution time of IO operations is long, the CPU idle time will be long, resulting in low CPU utilization. In this case, the size of thread pool can be increased appropriately.

Generally, you can configure twice the number of CPU cores.

Posted by desolator on Sun, 10 Oct 2021 16:16:47 -0700