Promotion -- 17 -- thread pool -- 03 -- source code analysis of ThreadPoolExecutor

Keywords: Java

Source code analysis of ThreadPoolExecutor

1. Interpretation of common variables

// 1. `ctl ', which can be regarded as an int type number, the upper 3 bits represent the thread pool status, and the lower 29 bits represent the number of worker s
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 2. `COUNT_BITS `, ` Integer.SIZE ` is 32, so ` COUNT_BITS ` is 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 3. 'capability', the maximum number of threads allowed in the thread pool. 1 shifts 29 bits to the left and then subtracts 1, which is 2 ^ 29 - 1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
// 4. The thread pool has five states, sorted by size as follows: running < shutdown < stop < tidying < terminated
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;

// Packing and unpacking ctl
// 5. 'runstateof()' to obtain the thread pool status. Through bitwise and operation, the lower 29 bits will all become 0
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 6. `workerCountOf() `, get the number of workers in the thread pool. Through bitwise and operation, all the upper 3 bits will become 0
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 7. `ctlOf() `, generate ctl value according to thread pool status and number of thread pool worker s
private static int ctlOf(int rs, int wc) { return rs | wc; }

/*
 * Bit field accessors that don't require unpacking ctl.
 * These depend on the bit layout and on workerCount being never negative.
 */
// 8. 'runstatelessthan()', the thread pool state is less than xx
private static boolean runStateLessThan(int c, int s) {
    return c < s;
}
// 9. 'runstateatleast()', the thread pool state is greater than or equal to xx
private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}

1 ctl, which can be regarded as an int type number. The upper 3 bits represent the thread pool status, and the lower 29 bits represent the number of worker s

  • Then why doesn't he use two values? He must have optimized them himself. If we let us write them ourselves, they must be two values. What is the current state of our thread pool, and then record how many threads are running here. However, if he combines the two values into one, the execution efficiency will be higher, Because these two values need thread synchronization, they are placed in one value. As long as one thread is synchronized, AtomicInteger will be more efficient than synchronized when there are a large number of threads and execution time is very short

4. The thread pool has five states, sorted by size as follows: running < shutdown < stop < tidying < terminated

  • RUNNING: normal operation;
  • Shutdown: called the shutdown method and entered the shutdown state;
  • STOP: call shutdown now to STOP it immediately;
  • TIDYING: shutdown is called, and then the thread is finished. The process being sorted out is called TIDYING;
  • TERMINATED: the whole thread is TERMINATED;

The following are some operations on ctl. runStateOf takes its state. workerCountOf calculates how many threads are working. The 8th and 9th runStateLessThan and runStateAtLeast are some things to help write code.

2. Construction method

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    // Basic type parameter verification
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    // Null pointer check
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    // According to the passed in parameters' unit 'and' keepAliveTime ', the survival time is converted into nanoseconds and stored in the variable' keepAliveTime '
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

3. Thread pool worker task unit

The work itself is Runnable and AQS

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // Here is the key to worker. A thread is created using a thread factory. The parameter passed in is the current worker
        this.thread = getThreadFactory().newThread(this);
    }

    /** Delegates main run loop to outer runWorker  */
    public void run() {
        runWorker(this);
    }

    // Omit code
}

Member variables:

- final Thread thread; ------ thread

- Runnable firstTask; ----- task

- volatile long completedTasks; ----- task

  • This work itself is Runnable and AQS. You can ignore AQS first, which doesn't matter, because it can be realized in other ways. The task itself is a Runnable. When you come in, he uses the Runnable to wrap it for you. Why wrap it? Because there are many states to be recorded in it, which you didn't have in the original task. In addition, this thing must run in the thread. Therefore, he wraps it for you again with Runnable.
  • Then a member variable will be recorded in the work class, which is thread. Which thread is executing my object? Many threads will rob, so this is why AQS is used. In addition, you also need to lock during the whole execution process. Otherwise, it is very possible for other threads to come in and ask your work to perform other tasks
    At this time, locking is also required, so AQS is required.

For this work class, you can simply regard it as a thread class, and then this thread class performs your own tasks.

4. Submit the process of executing task ---- execute()

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    // The number of workers is smaller than the number of core threads. Directly create workers to execute tasks
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // When the number of worker s exceeds the number of core threads, the task directly enters the queue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // The thread pool state is not RUNNING, which indicates that the shutdown command has been executed and the reject() operation needs to be performed on the newly added task.
        // The reason why check is needed here is that the state of the thread pool may change before and after the task is queued.
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // The reason why it is necessary to judge the value of 0 here is that in the thread pool construction method, the number of core threads is allowed to be 0
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // If the thread pool is not running or the task fails to enter the queue, try to create a worker to execute the task.
    // Here are three points to note:
    // 1. When the thread pool is not running, addWorker will judge the thread pool status internally
    // 2. The second parameter of addworker indicates whether to create a core thread
    // 3. If addworker returns false, it indicates that the task execution failed and the reject operation needs to be executed
    else if (!addWorker(command, false))
        reject(command);

execute() method

Step 1: if the number of core threads is not enough, start the core;

Step 2: add queue when the core thread is enough;

Part III: core threads and queues are full, non core threads;

  1. When executing a task, judge that the task is equal to empty throwing exceptions. This is very simple,
  2. The next step is to take the status value and calculate the number of threads in this value. Is the number of living threads less than the number of core threads? If it is less than addWorker, adding a thread is a difficult method. Its second parameter refers to whether it is a core thread. After all, if the number of cores is not enough, add a core thread first, Check this value again. We talked about starting a core thread with 0 for a task after the thread came up,
  3. The second is to put the number of core threads into the queue when they are full.
  4. Finally, when the core thread is full and the queue is full, start the non core thread. If it is less than the number of threads, it will be added directly. The logic executed later is not less than. If it is not less than the number of core threads, it will be thrown directly into the queue. workQueue.offer is to throw it into the queue and check the status. The state value may be changed in the middle, so double checking is required. This is the same logic as the DC in the singleton mode we talked about earlier. isRunning, take this state again. After you get this state, you need to perform a state switch. If it is not the Running state, it means that the shutdown command has been executed before you can convert this Running to another state. In other cases, if workerCountOf is equal to 0, it means that there are no threads in it. If there are no threads, I will add non core threads when the thread pool is Running normally. These steps can be seen through the source code.
  5. If adding work itself doesn't work, reject it.

5. addWorker source code analysis

  • addWorker is to add threads. Threads should be stored in the container. When adding threads, be sure to know that there may be many threads to throw into it, so you must synchronize them
  • Then, because it wants to pursue efficiency, it will not use synchronized. It will use lock or spin, which increases the complexity of your code.

addWorker is to add threads, which are stored in containers

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    // Outer spin
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // This condition is difficult to understand. I adjusted it to be equivalent to the following condition
        // (rs > SHUTDOWN) || 
        // (rs == SHUTDOWN && firstTask != null) || 
        // (rs == SHUTDOWN && workQueue.isEmpty())
        // 1. When the thread pool status is greater than SHUTDOWN, false is returned directly
        // 2. If the thread pool status is equal to SHUTDOWN and firstTask is not null, false will be returned directly
        // 3. If the thread pool status is equal to SHUTDOWN and the queue is empty, false is returned directly
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        // Inner spin
        for (;;) {
            int wc = workerCountOf(c);
            // If the number of worker s exceeds the capacity, false is returned directly
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // Increase the number of worker s by using CAS.
            // If the addition is successful, it will directly jump out of the outer cycle and enter the second part
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            // The thread pool state changes and spins the outer loop
            if (runStateOf(c) != rs)
                continue retry;
            // In other cases, spin directly in the inner circle
            // 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;
            // The addition of worker must be serial, so it needs to be locked
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                // You need to recheck the thread pool status here
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // If the worker has called the start() method, the worker will not be created
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // worker created and added to workers successfully
                    workers.add(w);
                    // Update 'largestPoolSize' variable
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // Start worker thread
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // The worker thread fails to start, which indicates that the thread pool state has changed (the shutdown operation is executed), and shutdown related operations are required
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorker does two things

  • First: add 1 to count first;
  • Second: it is really added to the task and start;

Let's read it roughly. He has done two steps in this. The whole addWorker source code has done two. The above two for loops only do the first step. This does one thing. Add 1 to the number of workers and add a worker. The number is in the 29 bits of 32 bits, and 1 is added in the case of multithreading, so he did two loops to do this. The outer loop sets the inner loop. He first took the status value, and then made a lot of judgments. If the status value does not match, return false. When does the status value not match, If it is greater than shutdown, it means that you have shut down, or you can add threads to all States except the above states. Adding threads is also an endless loop. First, calculate whether the current number of wc threads exceeds the capacity. If it exceeds the capacity, don't add it. Otherwise, add it in the way of cas. If the addition is successful, it means that the first step is completed, retry will break all the threads, and the inner loop of the outer loop will jump out. If it is not successful, get. After getting, reprocess it, continue retry is equivalent to continuously trying until we add this number to 1.

Then, the next step is to really start the work. New a work. After the work is new, start the thread. This work represents a thread. In fact, there is a thread in the work class. Locking is in a container. The multi-threaded state must be locked. After locking, check the state of the thread pool. Why check it, Because other threads may have been killed in the middle, see if this state is shut down, etc. if the conditions added to it are met, add it, and start running after adding this thread. This is a general logic of addWorker.

6. Core thread execution logic - runworker

runwork is how to execute this task after the thread is really started

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    // unlock() is called to allow external interrupts
    w.unlock(); // allow interrupts
    // This variable is used to determine whether a spin (while loop) has been entered
    boolean completedAbruptly = true;
    try {
        // Here is spin
        // 1. If firstTask is not null, execute firstTask;
        // 2. If firstTask is null, call getTask() to get the task from the queue.
        // 3. The characteristic of blocking queue is that when the queue is empty, the current thread will be blocked and wait
        while (task != null || (task = getTask()) != null) {
            // The purpose of locking the worker here is to achieve the following purposes
            // 1. Reduce lock range and improve performance
            // 2. Ensure that the tasks executed by each worker are serial
            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 the thread pool is stopping, interrupt the current thread
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            // Execute tasks, and expand their functions through 'beforeExecute()' and 'afterExecute()' before and after execution.
            // These two methods are empty in the current class.
            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 {
                // Help gc
                task = null;
                // Number of completed tasks plus one 
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // The spin operation is exited, indicating that the thread pool is ending
        processWorkerExit(w, completedAbruptly);
    }
}

  • runwork is how to execute this task after the thread is really started. Similarly, lock. What's more interesting is that this work inherits from AbstractQueuedSynchronizer and implements Runnable, which shows that work can be run in threads. At the same time, it is a lock and can be synchronized. In addition, it is a task that can be executed by threads

  • Why is it itself a lock? This work can be regarded as a worker waiting to be executed. It can throw content into many tasks, that is, multiple threads will access this object. When multiple threads access this object, they simply make a lock for themselves, so they don't define a lock themselves, Therefore, when you need to throw a task into the work, specify that my thread is the thread you are executing. Well, you can lock it through the work. There is no need to go to new locks. Therefore, when running the work, you need to lock it first. If you want to run, it has to lock it to execute, otherwise other threads may occupy the work, The following is another pile of execution. After execution, unlock comes out, and after execution + +.

Posted by bmx316 on Sat, 02 Oct 2021 16:01:57 -0700