Thread Pool Executor Source Code Analysis

Keywords: Programming less Java

In Article How to implement threads, source code analysis: Runnable, Thread, Callable, Future, FutureTask We know that there are three ways to implement threads. When threads are needed, threads can be created to execute tasks in these ways. If many tasks need to be executed at the same time, threads can be created to execute. However, the creation and destruction of threads is a time-consuming operation. If threads are created and destroyed frequently, the efficiency of the system will be affected. At this point, we can use the thread pool to perform tasks, that is to say, we can create several threads in advance, and if a task comes, we can take one of them to execute it. When the thread finishes the task, it does not destroy, but remains to perform the next task, so that we do not need to create and destroy threads frequently. In Java, ThreadPoolExecutor class is provided to implement the related operations of thread pool. Here's an analysis of the implementation principle of ThreadPoolExecutor class.

1: class diagram

If you want to see the source code of a class, first of all, you need to look at its class diagram, clarify its inheritance relationship, so that you may have an impression in your mind. The class diagram of ThreadPoolExecutor is as follows:

Executor 

The top-level interface implemented by ThreadPoolExecutor is Executor interface, which defines only one method, execute(), to execute threads. Note that this method has no return value and may throw Rejected Execution Exception and NullPointerException exceptions.

public interface Executor {

    /**
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

ExecutorService

ExecutorService inherits Executor, which is also an interface that provides the following methods.

public interface ExecutorService extends Executor {

    //Close the thread pool and the previously submitted tasks will be executed, but no new tasks will be accepted. If it is closed, the call again has no other effect.
    //This method does not wait for previously submitted tasks to complete.
    void shutdown();

    //Trying to stop all tasks that are being performed will not process the tasks that are waiting and return to the list of tasks that are waiting to be performed.
    List<Runnable> shutdownNow();

    //Is it closed?
    boolean isShutdown();

    //If all tasks are completed after shutdown is invoked, true is returned. 
    //It is worth noting that isTerminated will never be true unless shutdown or shutdown Now is called first.
    boolean isTerminated();

    //When shutdown is invoked, it blocks until all tasks are computed, or the timeout or the thread is interrupted.
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

    //Submit the Callable task to the thread pool and return the result of the task
    <T> Future<T> submit(Callable<T> task);

    //Submit the Runnable task to the thread pool and return the result
    <T> Future<T> submit(Runnable task, T result);

    /Submission Runnable Tasks are executed in the thread pool and results are returned/
    Future<?> submit(Runnable task);

    //Execute all tasks and return the result set
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

    //Execute all tasks, return result set, timeout
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit) throws InterruptedException;

    //Execute any task in the task set
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

    //Execution of any task in the task set, with timeouts
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ExecutorService also provides the submit() method for submitting tasks, which differs from the execute() method in that submit() can have a return value, while the execute() method has no return value.

AbstractExecutorService

AbstractExecutorService implements the ExecutorService interface and provides default implementations of some methods, as well as some additional methods:

public abstract class AbstractExecutorService implements ExecutorService {

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

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

ThreadPoolExecutor

Next, look at ThreadPoolExecutor, which is a major class of thread pool implementations.

2: The state of thread pool

In the ThreadPoolExecutor class, several variables are provided to represent the state of the thread pool:

    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int RUNNING    = -1 << COUNT_BITS; // -536870912
    private static final int SHUTDOWN   =  0 << COUNT_BITS; // 0
    private static final int STOP       =  1 << COUNT_BITS; // 536870912
    private static final int TIDYING    =  2 << COUNT_BITS; // 1073741824
    private static final int TERMINATED =  3 << COUNT_BITS; // 1610612736

These states are mainly represented by runState. Look at what these states mean.

RUNNING: Represents that the thread pool can accept new tasks and process queued tasks

SHUTDOWN: Do not accept new tasks, but process queued tasks

STOP: No new tasks, no queuing tasks

TIDYING: All tasks are terminated, workerCount is zero, and the thread that transitions to state TIDYING runs the terminate () hook method

TERMINATED: terminate() method completed

The transitions between these five states are as follows:

When the shutdown() method is called, the state changes from RUNNING to SHUTDOWN, which may also occur in the finalize() method.

When the shutdownNow() method is called, the state changes from RUNNING or SHUTDOWN to STOP.

When the tasks in the queue and thread pool are empty, the state changes from SHUTDOWN to TIDYING

When there are no tasks in the queue, the state changes from STOP state to TIDYING state

When the terminate() method has been completed, the state changes from TIDYING to TERMINATED

The conversion diagram is as follows:

3. Construction method

Next, look at some of the attributes of ThreadPoolExecutor:

// workQueue is mainly used to store tasks, any BlockingQueue can be used to save tasks; if the current number of threads is less than the corePoolSize core
// If the number of threads currently running is greater than or equal to the size of core PoolSize core thread pool,
// Submitted tasks are placed in a queue; there are generally three types of selection queues:
// 1: SynchronousQueue
// 2: LinkedBlockingQueue
// 3: ArrayBlockingQueue
private final BlockingQueue<Runnable> workQueue;

// lock
private final ReentrantLock mainLock = new ReentrantLock();

// Work set, accessible only by acquiring locks
private final HashSet<Worker> workers = new HashSet<Worker>();
// Maximum number of threads ever appeared in the thread pool
private int largestPoolSize;
// Number of tasks completed 
private long completedTaskCount;

// Thread-creating factories, if not provided, default is to use the default factories, they belong to a thread group, the same priority, etc., can be customized.
private volatile ThreadFactory threadFactory;

// Rejection policy: How will the submission task be handled when the thread pool is closed, or when the queue is full and the number of threads has reached the maximum number allowed by the thread pool?
// The ThreadPoolExecutor class provides four internal classes for processing:
// 1: AbortPolicy: This policy throws an exception. (default)
// 2: CallerRunsPolicy: It runs the rejected task directly in the calling thread of the execute method. If the thread pool is closed, the task will be discarded.
// DiscardPolicy: Discarding Tasks Directly
// 4: Discard Oldest Policy: Discard the earliest unprocessed task and execute it
private volatile RejectedExecutionHandler handler;

// Thread lifetime, when the number of threads in the thread pool is greater than corePoolSize and no longer perform tasks, the maximum inventory time is destroyed when it exceeds time.
private volatile long keepAliveTime;

// If false (default), the core thread will remain active even if it is idle. 
// If true, the core thread uses keepAliveTime to wait for work over time.
private volatile boolean allowCoreThreadTimeOut;

// The size of the core thread pool, which will never be destroyed unless allowCoreThreadTimeOut is set
private volatile int corePoolSize;

// Maximum Thread Pool Size
private volatile int maximumPoolSize;

After understanding the above definition of attributes, let's look at the construction method of ThreadPool Executor, which provides four construction methods:

Let's look at the definition of the fourth construct. The other three call the fourth construct and some parameters provide default values.

public ThreadPoolExecutor(int corePoolSize, // Core thread pool size
						  int maximumPoolSize, // Maximum thread pool size
						  long keepAliveTime, // Thread lifetime
						  TimeUnit unit, // Life preservation time unit
						  BlockingQueue<Runnable> workQueue, // Cache queues for storing tasks
						  ThreadFactory threadFactory, // Thread factory
						  RejectedExecutionHandler handler)  // Refusal strategy
{
	if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
		throw new IllegalArgumentException();
	if (workQueue == null || threadFactory == null || handler == null)
		throw new NullPointerException();
	this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
	this.corePoolSize = corePoolSize;
	this.maximumPoolSize = maximumPoolSize;
	this.workQueue = workQueue;
	this.keepAliveTime = unit.toNanos(keepAliveTime);
	this.threadFactory = threadFactory;
	this.handler = handler;
}

The constructed parameters are the attributes mentioned above, and the functions of each parameter are explained above.

When a task is submitted to the thread pool, how will the task be executed? Next, let's look at how to execute the task after it has been submitted. After the task is submitted to the thread pool, it will be executed by the execute() method. Although submit() can also submit the task to the thread pool for execution, during the resolution of the AbstractExecutorService class above, submit will also call execute() to perform the task. Next, look at a flow of the execute() method:

// To execute tasks, tasks may create new threads to execute, or they may take out existing threads from the thread pool to execute; if the thread pool is closed or the cache queue is full, and the number of threads in the thread pool reaches the maximum allowable value,
// Submitted tasks will be handled by Rejected Execution Handler
// There are three steps to submit a task:
// 1: If the number of threads running in the thread pool is less than corePoolSize, a new thread is created to perform the task.
// 2: If the current number of worker threads is greater than or equal to corePoolSize, the task will be inserted into the cache queue. If the task inserts into the queue successfully, it still needs to check again whether the task should be added. If the thread pool has been closed, etc.
// 3: If the queue is full and adding tasks to the queue fails, a new thread is attempted to execute the task. If the number of threads in the thread pool has reached maximum PoolSize, it will be processed by Rejected Execution Handler.

public void execute(Runnable command) {
	if (command == null)
		throw new NullPointerException();
	
    // The state of the thread pool is mainly controlled by ctl, which records runState and workCount
	int c = ctl.get();

    // If the current number of worker threads is less than corePoolSize, a new thread is created to perform the task and put the thread into the thread pool.
	if (workerCountOf(c) < corePoolSize) {
        // Adding tasks, the second parameter indicates whether to limit the number of adding threads based on corePoolSize or maximumPoolSize.
        // If true, judge by corePoolSize; if false, judge by maximumPoolSize
		if (addWorker(command, true))
			return;
        // Failed to add and retrieve status
		c = ctl.get();
	}
    // If the current thread pool state is running and the task is added to the queue successfully
	if (isRunning(c) && workQueue.offer(command)) {
        // Get the state of the thread pool again
		int recheck = ctl.get();
        // If the state of the thread pool is not running, the task needs to be deleted because it has been added to the queue; then the task is processed by Rejected Execution Handler
		if (! isRunning(recheck) && remove(command))
			reject(command);
        // If the thread pool is still running and the number of threads working is 0, add tasks.
        // The second parameter is false, setting the upper limit of the limited number of threads in the thread pool to maximumPoolSize
		else if (workerCountOf(recheck) == 0)
			addWorker(null, false);
	}
    //If implemented here, there are two situations:
    // 1. Thread pool is no longer running state;
    // 2. Thread pool is running, but workerCount >= corePoolSize and workQueue is full, then the addWorker method is called again.
    // But the second parameter is passed in false, setting the upper limit of the limited number of threads in the thread pool to maximumPoolSize; if it fails, the task is handled by Rejected Execution Handler
	else if (!addWorker(command, false))
		reject(command);
}

The process of adding tasks mentioned above is summarized as follows:

1. If the number of threads in the current thread pool is less than corePoolSize, new threads are created to perform tasks

2. If the number of threads in the current thread pool is greater than or equal to corePoolSize and the cache queue is not full, insert the task into the queue

3. If the number of threads in the current thread pool is greater than or equal to corePoolSize and less than the maximum number allowed by the thread pool and the queue is not full, a new thread is created to perform the task.

4. If the number of threads in the current thread pool is greater than the maximum number allowed by the thread pool and the queue is full, there is a rejection policy.

In the ThreadPoolExecutor class, there is also an internal class Worker class, which is mainly used to maintain the thread interruption status of running tasks. It implements Runnable and AbstractQueuedSynchronizer, which means that it is also a thread. For the queue synchronizer AbstractQueuedSynchronizer, you can see the article. Lock Lock Lock Source Code Analysis

    // Each thread in the thread pool is encapsulated as a Worker object, and ThreadPool maintains a set of Worker objects.
    private final class Worker extends AbstractQueuedSynchronizer implements Runnable
    {

        // The running thread can be null, that is, threads are threads created by ThreadFactory when the constructor is called, and threads are threads used to process tasks.
        final Thread thread;
        // To run the task for the first time, null, or first Task, can be used to save the incoming task
        Runnable firstTask;
        // Task counter per thread
        volatile long completedTasks;

        //Create worker
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            // Create threads using thread factories
            this.thread = getThreadFactory().newThread(this);
        }

        // Thread of operation
        public void run() {
            runWorker(this);
        }

        // Whether to acquire locks
        // 0 means no lock was acquired
        // 1 denotes acquisition of locks

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
        // Attempt to get the lock, get back true, get back false
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // Unlock
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // Method of calling queue synchronizer
        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) {
                }
            }
        }
    }

This is the ThreadPool Executor analysis:

Reference resources: Deep Understanding of Java Thread Pool Executor

Posted by danzene on Wed, 15 May 2019 13:24:59 -0700