ThreadPoolExecutor Core Source Parsing

Posted by jenni on Sat, 11 May 2019 11:46:10 +0200

This article only introduces the key parts of ThreadPool Executor source code. At the beginning, we introduce some core constants in ThreadPool Executor, and then select several key methods in the thread pool work cycle to analyze its source code implementation. In fact, the best way to see the JDK source code is to look at class file annotations, the author wrote everything he wanted to say in it.

Some important constants

The internal author of ThreadPool Executor uses a 32-bit int value to represent the running state of the thread pool and the number of threads in the current thread pool. This variable is called ctl (abbreviation for control). The upper 3 bits represent the allowable state, and the lower 29 bits represent the number of threads (up to 2 ^ 29 - 1 threads are allowed).

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3; // 29 place
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1; // Maximum Thread Pool Capacity

    // runState is stored in the high-order bits
    // Defined thread pool state constants
    // 111 + 29 zeros, with a value of - 4 + 2 + 1 = 1 (unknown wall)
    private static final int RUNNING    = -1 << COUNT_BITS; 
    // 000+29 0
    private static final int SHUTDOWN   =  0 << COUNT_BITS; 
    // 001+29 0
    private static final int STOP       =  1 << COUNT_BITS; 
    // 010+29 0
    private static final int TIDYING    =  2 << COUNT_BITS; 
    // 011+29 0
    private static final int TERMINATED =  3 << COUNT_BITS; 

    // Packing and unpacking ctl
    private static int runStateOf(int c)     { return c & ~CAPACITY; } // Get thread pool status
    private static int workerCountOf(int c)  { return c & CAPACITY; } // Get the number of threads in the thread pool
    private static int ctlOf(int rs, int wc) { return rs | wc; } // Reversely construct the value of ctl

Because constants representing the state of thread pools can represent order by the size of values, there will be:

rs >= SHUTDOWN // That means SHUTDOWN, STOP or TIDYING or TERMINATED, not RUNNING anyway.

Understanding the constants above will help you understand the source code later.

Discuss state transition of thread pool

We already know from the first section that thread pools are divided into five states. Now let's talk about how these five states limit the behavior of thread pools.

  1. RUNNING: New tasks are acceptable and tasks in Queue are executed
  2. SHUTDOWN: No longer accepts new tasks, but can continue to perform tasks already in Queue
  3. STOP: No longer accept new tasks, and no longer perform tasks that already exist in Queue
  4. TIDYING: All tasks completed, workCount=0, thread pool status changed to TIDYING and hook method, terminated()
  5. TERMINATED: ``hook method terminated() state entered after execution

Key Logic of Thread Pool

The figure above summarizes the key steps in ThreadPool Executor source code, which corresponds to the core source code we parsed (see watermarking in the source of the figure above).

  1. The execute method is used to submit tasks to the thread pool, which is the first step for users to use the thread pool. If the thread pool does not reach corePoolSize, a new thread is created, the task is set to the thread's first Task, and then the workerSet is added to wait for scheduling, which requires acquiring the global lock mainLock.
  2. Once corePoolSize has been reached, put task into the blocking queue
  3. If the blocking queue does not fit, a new thread is created to handle it. This step also requires the acquisition of the global lock mainLock.
  4. The current thread pool workerCount is processed with rejectHandler after it exceeds maxPoolSize

We can see that the design of thread pool makes it possible to avoid using global lock in step 2. It only needs to jam into the queue and return to wait for asynchronous scheduling. Only 1 and 3 need to acquire global lock when creating threads. This is conducive to improving the efficiency of thread pool, because a thread pool always spends most of the time in step 2, otherwise the thread pool has no significance.

Source code analysis

This article only analyses execute, addWorker, runWorker, three core methods and a Worker class, understand these, in fact, other code can understand.

Class Worker

    // Implementing simple lock control by inheriting from AQS
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        // The thread where the worker runs
        final Thread thread;
        // The first task assigned to the thread may be null, which runs if it is not null.
        // If it's null, go to Queue to get the task by getTask method
        Runnable firstTask;
        // Number of tasks completed by threads
        volatile long completedTasks;

        Worker(Runnable firstTask) {
        // Restrict threads until the runWorker method does not allow interruption
            setState(-1); 
            this.firstTask = firstTask;
            // Thread Factory Creates Threads
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            // The run method inside the thread calls the runWorker method
            runWorker(this);
        }
    }

execute method

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        // If the current number of threads is less than corePoolSize
        if (workerCountOf(c) < corePoolSize) {
        // Call the addWorker method to create a new thread. If the new thread returns true successfully, the execute method ends.
            if (addWorker(command, true))
                return;
            // This means that addWorker fails and executes downward because addWorker may change the value of ctl.
            // So here we recapture the ctl
            c = ctl.get();
        }

        // At this point either corePoolSize is full or addWorker fails
        // The former is well understood. Why did the latter fail? In addWorker

        // If the thread pool status is RUNNING and task inserts Queue successfully
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // If you are no longer in the RUNNING state, delete the task that has entered the queue and then execute the rejection policy
            // The main concern here is that other threads in the concurrent scenario change the state of the thread pool, so under double-check
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // This branch is somewhat difficult to understand, meaning that if the current workerCount=0, create a thread
            // So why does the addWorker(command, true) at the beginning of the method return false?
            // Here's a scene of new Cached ThreadPool, core PoolSize = 0, maxPoolSize=MAX.
            // You will enter this branch and create temporary threads bounded by maxPoolSize, firstTask=null
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // This branch is well understood, workQueue is full, so create threads based on maxPoolSize
        // If it's not possible to create maxPoolSize indicating that it's full, execute the rejection policy
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker method

    // core denotes whether corePoolSize or maxPoolSize is bounded
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // See when addWorker returns false
            // The if logic here is a little hard to understand. With the allocation rate in mathematics, you can put the first logical expression in parentheses.
            // 1. RS >= SHUTDOWN & rs!= SHUTDOWN actually means that when the thread pool state is STOP, TIDYING, or TERMINATED, of course, no worker can be added. Do you want to add a worker when the task is not executed?
            // 2. RS >= SHUTDOWN & & first Task!= null means that when a non-empty task is submitted, but the thread pool state is no longer RUNNING, of course, you cannot add Worker, because you can only perform the tasks already in Queue at most.
            // 3. RS >= SHUTDOWN & workQueue. isEmpty () If Queue is empty, no new additions are allowed.
            // It should be noted that if rs = SHUTDOWN & & & firstTask = null or rs = SHUTDOWN & workQueue is not empty, worker can be added, and temporary threads need to be created to handle tasks in Queue.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // It's also a case of returning false, but it's very simple: the number overflows.
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // When CAS succeeds, it jumps out of the loop
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // If CAS fails, check the current thread pool status. If it changes, go back to the outer loop again. That's understandable. Otherwise, if CAS fails but the thread pool status remains unchanged, just continue the inner loop.
                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) {
                // This is a global lock and must be held in order to perform addWorker operations.
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    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) {
                    // Startup thread
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

runWorker Method

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // Loop until task = null, possibly due to thread pool closure, waiting timeout, etc.
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // The if logic below is not well understood... Translated the following notes
                // If the thread pool stops, ensure that the thread interrupts.
                // If not, make sure the thread is uninterrupted. This requires recapturing ctl in the second case to handle shutdownNow competition when interrupts are cleared
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // Pre-hook function, you can customize
                    beforeExecute(wt, task); 
                    Throwable thrown = null;
                    try {
                        // Running run Method
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        // Throwable is not allowed to be thrown by thread run, so it is converted to Error 
                        thrown = x; throw new Error(x);
                    } finally {
                    // Posthook functions can also be customized
                        afterExecute(task, thrown);
                    }
                } finally {
                    // Get the next task
                    task = null;
                    // Increase the number of tasks completed
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

summary

After reading the source code of ThreadPool Executor, I have to marvel at the elegance of the code, but because it is too concise and elegant to find a verbose code, it is a bit difficult to understand. The suggestion of looking at the source code is to read the class annotations carefully first, then cooperate with debug to clarify what the key steps are doing. Some corner case s are mixed in the main logic. If you don't understand them at first, you can skip them directly, and then reflect on them afterwards.

Topics: JDK less