Analyze the implementation principle of thread pool (pooling Technology) from the source code

Posted by digitalhuman on Sat, 12 Feb 2022 02:43:50 +0100

Thread pool is a very important knowledge point and a typical application of pooling technology. I believe many people have the experience of using thread pool, but do you know the implementation principle of thread pool? In this article, we will explore the source code of thread pool.

Origin of thread pool

Background: with the upgrading of computer hardware, our software has the ability of multithreading. When we are doing multithreaded programming, we need to create threads. If the program concurrency is very high, we will create a large number of threads, and each thread will finish executing a task in a short time. In this way, frequent thread creation will greatly reduce the system performance and increase the server overhead, because creating threads and destroying threads require additional consumption.

At this time, we can use pooling technology to optimize this defect, and thread pool is born.

The essence of pooling technology is that in high concurrency scenarios, in order to realize resource reuse and reduce the overhead of resource creation and destruction, if the number of concurrency is very small, there is no obvious advantage (resources always occupy system memory and have no chance to be used).

Introduction to pooling Technology: when is pooling technology? Pooling technology is a programming skill. When the program has high concurrency, it can obviously optimize the program and reduce the additional overhead of the system, such as frequent creation and destruction of connections. The pooling technologies we often come into contact with include database connection pool, thread pool, object pool and so on. The characteristic of pooling technology is to maintain some high-cost resources in a specific pool (memory), and specify the minimum number of connections, maximum number of connections, blocking queue, overflow rules and other configurations to facilitate unified management. In general, some supporting functions such as monitoring and forced recycling will also be attached.

As a resource utilization technology, pooling technology is typically used in the following situations:

  • When the cost of obtaining resources is high
  • When the frequency of requesting resources is high and the total number of resources used is low
  • In the face of performance problems, it involves dealing with time delays

Pool technology resource classification:

  • System resources called by the system, such as threads, processes, memory allocation, etc
  • Remote resources of network communication, such as database connection, socket connection, etc

Definition and use of thread pool

Thread pool is born to avoid the extra cost of creating threads and destroying threads. Therefore, after we define the thread pool, we don't need to create threads ourselves, but use thread pool calls to execute our tasks. Let's take a look at how to define and create a thread pool.

Scheme 1: Executors (only for understanding, scheme 2 is recommended)

Executors can be used to create thread pools. A series of factory methods are provided to create thread pools. The returned thread pools implement the ExecutorService interface.

ExecutorService interface is a subclass interface of Executor interface, which is more widely used. It provides a method for thread pool life cycle management and returns Future objects.

In other words, we create a thread pool through Executors to get ExecutorService, and execute asynchronous tasks through ExecutorService (implement Runnable interface)

Executors can create the following types of thread pools:

  • newCachedThreadPool creates a cacheable thread pool. If the number of threads in the thread pool is excessive, the excess thread resources will be recycled after 60 seconds. When the task book increases and the threads are not enough, a new thread will be created.
  • newFixedThreadPool creates a fixed length thread pool, which can control the maximum concurrent number of threads. The exceeded threads will wait in the queue.
  • newScheduledThreadPool creates a fixed length thread pool that supports scheduled and periodic task execution.
  • Newsinglethreadexecution creates a single thread thread pool and uses only one thread to execute tasks, which can ensure that tasks are completed in the order of submission.

Scheme 2: ThreadPoolExecutor

Alibaba development specification stipulates that thread pools are not allowed to be created through Executors, but through ThreadPoolExecutor.

Benefits: let students write more clearly the running rules of thread pool and avoid the risk of resource depletion.

Seven parameters of ThreadPoolExecutor:

  1. corePoolSize is the number of core threads. The core threads will always be retained and will not be destroyed.

  2. maximumPoolSize is the maximum number of threads. When the core thread cannot meet the needs of the task, the system will create a new thread to execute the task.

  3. keepAliveTime is the survival time. Threads other than the core thread will be destroyed as long as they are idle.

  4. timeUnit represents the time unit of thread survival.

  5. BlockingQueue blocking queue

    If the task being executed exceeds the maximum number of threads, it can be stored in the queue. When there are free resources in the thread pool, the task can be taken out of the queue to continue execution.

    There are several types of queues: LinkedBlockingQueue ArrayBlockingQueue SynchronousQueue TransferQueue.

  6. threadFactory thread factory is used to create threads and customize threads. For example, we can define thread group names, which is very helpful in troubleshooting jstack problems.

  7. rejectedExecutionHandler reject policy,

    When all threads (maximum number of threads) are busy and the task queue is full, the reject policy is executed.

    JDK provides us with four rejection strategies, which we must be familiar with

    • AbortPolicy: discards the task and throws an exception RejectedExecutionException. default
    • DiscardPolicy: discard the latest task without throwing exceptions.
    • Discard oldest policy: discard the tasks with the longest queue, that is, the oldest tasks.
    • CallerRuns: the task is processed by the caller (the thread submitting the asynchronous task).

Implementation principle of thread pool

To implement a thread pool, we need to care about the ThreadPoolExecutor class, because Executors create thread pools through the new ThreadPoolExecutor object.

Looking at the class inheritance relationship of ThreadPoolExecutor, we can see why the return result of thread pool created by Executors is ExecutorService, because ThreadPoolExecutor is the implementation class of ExecutorService interface, and the essence of thread pool created by Executors is the created ThreadPoolExecutor object.

Let's take a look at the source code of ThreadPoolExecutor. First, the variables and constants defined in ThreadPoolExecutor:

    // A compound type variable is an atomic integer that controls the state (running state | number of active threads in the thread pool)
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 
    private static final int COUNT_BITS = Integer.SIZE - 3; // Lower 29 bits 
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1; // capacity
    // The operating status is stored in the upper 3 bits
    private static final int RUNNING    = -1 << COUNT_BITS;  // Accept new tasks and process queue tasks
    private static final int SHUTDOWN   =  0 << COUNT_BITS;  // New tasks are not accepted, but queue tasks are processed
    private static final int STOP       =  1 << COUNT_BITS;  // Do not accept new tasks, do not process queue tasks, and interrupt the tasks being processed
    private static final int TIDYING    =  2 << COUNT_BITS;  // All tasks have ended, the active thread is 0, the thread transitions to TIDYING state, and the terminated() hook method will be executed
    private static final int TERMINATED =  3 << COUNT_BITS;  // The terminated() method has completed
​
    // How to set ctl parameters
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }
​
    /**
     * Blocking queue
     */
    private final BlockingQueue<Runnable> workQueue;
​
    /**
     * Lock Lock
     */
    private final ReentrantLock mainLock = new ReentrantLock();
​
    /**
     * Workers
     */
    private final HashSet<Worker> workers = new HashSet<Worker>();
​
    /**
     * Waiting conditions support waiting termination
     */
    private final Condition termination = mainLock.newCondition();
​
    /**
     * Maximum pool size
     */
    private int largestPoolSize;
​
    /**
     * Number of tasks completed
     */
    private long completedTaskCount;
​
    /**
     * Thread factory
     */
    private volatile ThreadFactory threadFactory;
​
    /**
     * Reject strategy
     */
    private volatile RejectedExecutionHandler handler;
​
    /**
     * survival time 
     */
    private volatile long keepAliveTime;
​
    /**
     * Number of core threads allowed
     */
    private volatile boolean allowCoreThreadTimeOut;
​
    /**
     * Number of core threads
     */
    private volatile int corePoolSize;
​
    /**
     * Maximum number of threads
     */
    private volatile int maximumPoolSize;
​
    /**
     * Default reject policy
     */
    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
​
    /**
     * shutdown and shutdownNow jurisdiction
     */
    private static final RuntimePermission shutdownPerm =
        new RuntimePermission("modifyThread");

Constructor, which supports a minimum of five parameters and a maximum of four constructors with seven parameters:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
​
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
​
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
​
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
​

Workers, who perform tasks in the thread pool. The thread pool works through these workers, including core employees (core threads) and temporary workers (temporarily created when there are not enough staff. If there is a free time factory, they will be laid off),

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
​
        private static final long serialVersionUID = 6138294804551838833L;
​
        // The essence of a worker is a thread
        final Thread thread;
​
        // The first task
        Runnable firstTask;
        
        volatile long completedTasks;
​
        /**
         * constructor 
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
​
        /** work  */
        public void run() {
            runWorker(this);
        }
​
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
​
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
​
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
​
        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }
​
        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

The core method is to execute tasks through the thread pool (this is also the operation principle of the thread pool)

  • Inspection task
  • Get current thread pool status
  • Judge whether the number of core workers is less than the number of core workers
  • If less than, hire people and arrange work
  • If it is not less than, judge whether the waiting area is full
  • If not, the task will be discharged into the waiting area
  • If the queue is full, see whether recruitment is allowed. If recruitment is allowed, recruit temporary workers
  • If not, the thread pool cannot receive new tasks and starts to execute the rejection policy according to the rejection policy agreed by the boss
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
​
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        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);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

The submit () method is defined by its abstract parent class. Here we can clearly see the difference between submit and execute. Through the submit call, we will create RunnableFuture and return Future. Here we can tell the submit method the return value type, and it will return the value through generic constraints.

public abstract class AbstractExecutorService implements ExecutorService {
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
​
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
​
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
    ...
}       

addWorker() is a way to recruit people

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
​
            // Judgment status and task list
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
​
            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
​
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
​
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

Method of obtaining task

    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
​
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
​
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
​
            int wc = workerCountOf(c);
​
            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
​
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
​
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

The method of letting employees work, assigning tasks and running tasks

   final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

Finally

The article is over. If you are helpful, you can pay attention to the official account of the WeChat public.

 

 

Topics: Java network Back-end Interview Programmer