Thread pool source code analysis

Keywords: Java

Thread pool source code analysis is mainly divided into two parts, with the following problems to analyze

1. Creation of thread pool: how are threads in the thread pool created? How the task is performed. How to reuse after executing the task. When the task is squeezed, how to deal with these tasks? It is mainly expanded with execute() as the starting point

2. Destruction of thread pool: how to gracefully close a thread pool? If the thread pool is closed, how will the executing tasks and tasks waiting to be executed be processed? Also, if the thread pool is closed and continues to submit tasks, how will the thread pool be processed? It mainly starts with shutdown() and shutdown now ()

1. How are tasks added and executed

1. Representation of status bit and thread number of thread pool

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

//29
private static final int COUNT_BITS = Integer.SIZE - 3;
//A total of 29 1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
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
//Gets the status of the thread pool
private static int runStateOf(int c)     { return c & ~CAPACITY; }
//Gets the number of threads in the thread pool
private static int workerCountOf(int c)  { return c & CAPACITY; }
//The initialization variable ctl is initialized with two numbers or operations. The upper 3 bits represent the status and the lower 29 bits represent the number of threads in the thread pool
private static int ctlOf(int rs, int wc) { return rs | wc; }

The design here is such that the high 3 bits in an int type variable ctl are used to represent the status bit of the thread pool and the low 29 bits are used to represent the number of threads in the current thread pool

2. Status of thread pool

  • RUNNING indicates that the current thread is RUNNING and can accept new submitted tasks
  • SHUTDOWN does not accept new tasks, but will complete the tasks to be executed in the blocking queue. It is generally generated through shutdown()
  • STOP does not accept new tasks, and the tasks to be executed in the blocking queue are not executed. It is generally generated through shutdown now()
  • All tasks in the TIDYING thread pool have ended, and the number of threads is 0. Start calling the terminated() temporary state
  • The TERMINATED terminated() method call is complete

3. Submit tasks

Before analyzing, remember a few points,

1. Thread pool operates in a multithreaded environment, so concurrency should be considered in the code.

2. The number of core threads can be set with timeout. If the task queue is blocked for more than a certain time, it can be interrupted. In other words, it can be set to no active core threads, but the thread pool status is still running, but the number of core threads is 0. PS: use allowCoreThreadTimeOut(boolean value) to set the running setting,

3. Many operations of thread pool source code assume success first, and then roll back the previous operations if they fail. For example, add a task to create a thread, add the task to the queue at the beginning, and remove the task from the queue if the following conditions are not met

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //Judge whether the number of threads in the thread pool is less than the number of core threads. You need to create core threads to execute tasks
        if (workerCountOf(c) < corePoolSize) {
            //Add task
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //Execution here indicates that the number of previous core threads is full. If the thread pool is still running, try to put the task into the blocking queue
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //What is the scenario here? Why do you need to recheck the thread pool status? There may be a task queue that just put the task into the thread pool, and then the thread pool is closed. At this time, you need to remove the task from the queue (concurrency)
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //What scene is this? The number of core threads is 0? This is what we said earlier. We can set the number of core threads to be destroyed after completing the task, and then the number of core threads will be 0. How to execute the task in the queue just now, we need to create the number of non core threads to execute the task (it can be ignored because it will not be set like this)
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
       //This indicates that the number of core threads is full and the queue is full. Create non core threads to execute tasks
        else if (!addWorker(command, false))
            reject(command);
    }

OK, throw out the tasks in the thread pool and add detailed logic. We roughly know the processing logic when a task is added to the thread pool

  1. Judge whether the current number of threads is less than the number of core threads. If less than, create a core thread and execute the task. Otherwise, proceed to step 2
  2. If the thread pool is still running, add the task to the queue. If the addition is successful, perform the following operations; otherwise, perform step 3
    1. Judge the thread pool status again. If it is not running now, remove the task just added to the queue
    2. Judge the number of threads in the thread pool. If the number is 0, the number of non core threads will be added (ignore this situation, and the number of core threads can be destroyed will not be configured under normal circumstances)
  3. Add a new number of non core threads to execute the task. If it fails, use the reject policy to process the task

Let's start a detailed analysis of addWorker()

/**
**  firstTask Indicates the task to be executed. If it is empty, it indicates that the task is taken from the queue for execution. Otherwise, it takes firstTask for execution
**  core Whether to create a core thread and execute tasks
**/
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
    // The following for() is mainly used for verification. If the verification passes, the number of threads in the thread pool will be + 1. Note that only the number is updated, but the actual thread has not been created
        for (;;) {
            int c = ctl.get();
            //Get current thread running status
            int rs = runStateOf(c);
            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;
                //If the previous cas fails, you need to read it again
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }
		//Let's start creating a thread, and then use the newly created thread to process the task
        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 {
                    //Gets the current thread state
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //Add work to the thread pool collection
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //Start the thread to execute the task
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

We analyze it paragraph by paragraph

if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
   return false;

If RS > = shutdown is satisfied, it means that the current thread pool is at least in the shutdown state. Note that new tasks cannot be added in this state,

! (rs = = shutdown & & firsttask = = null & &! Workqueue. Isempty()) means

  1. Thread pool is in shutdown state
  2. The newly added task is empty (indicating that the task is taken out from the task queue for execution)
  3. Task queue is not empty

If one of the three is not satisfied, the whole if() returns true, which means that the task addition fails. In fact, it is

  1. It means that after the current thread pool is in SHUTDOWN state, if the new task is not empty, it will be returned directly, indicating that adding the task failed
  2. Indicates that after the current thread pool is in SHUTDOWN state, the number of core threads is 0, and the number of non core threads is added to process tasks, but the queue is empty and returned directly, indicating that adding tasks failed
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;
}
  • If the current number of threads has reached the maximum capability, return directly
  • If the core thread is added, but the current thread data has reached the core, it will be returned directly
  • If the number of non core threads added, but the current number of threads has reached maximumPoolSize, it is returned directly

All the previous checks have passed. Start adding the number of threads in the thread pool. compareAndIncrementWorkerCount(c) is added through CAS. If it fails, retry. Otherwise, directly jump out of the largest loop outside.

break retry jumps out of the outermost loop. A simple break can only jump out of the current loop

Encapsulate the incoming task into a worker object. Creating a worker object actually creates a thread through the thread factory

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

    public void run() {
        runWorker(this);
    }
}

The following series of operations will eventually call t.start(); Start the thread and execute the task, that is, indirectly call runWorker(this);

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 ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //Or did you call the run() of the incoming task
                        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 {
                    //If the final task assignment is empty, the next cycle will take the task directly from the queue
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            //It is important here that if there is an exception in our task, this code will not be executed.
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

There is a key point in the above code, completedAbruptly = false; When will it not be executed? Because there is no catch here, so

  • task.run(); exception occurred
  • beforeExecute(wt, task); exception occurred
  • afterExecute(task, thrown); exception occurred

Why the exception in getTask() is not mentioned, because the code implementation is caught by the try... Catch and will not throw an exception. (the implementation here will be described in detail later)

So in a word, completedAbruptly = false; Only when an exception is thrown in the user's own task, manually closing the thread pool will not be affected

Get task getTask() detailed analysis

    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);
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
          	//If the core thread is allowed to time out and has timed out and the task in the queue is empty, the thread data will be reduced directly, and the loop will return empty
            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 front is some verification, the most critical code

try {
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    timedOut = false;
}
  1. Here is the key to thread reuse. If the allow core thread timeout is set, workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS), the timeout will be returned. Threads are no longer reused
  2. If the timeout is not set, wait for workQueue.take() all the time
  3. In either case, interrupt exit is supported. How to interrupt the thread pool will be described in detail later

Then let's look at the final code to be executed, processworkerexit (W, completed abruptly); The logic inside (the premise of this code execution is that the task is abnormal, or the thread is interrupted and jumps out of the task queue to wait for execution, otherwise it will be blocked to get the task from the task queue all the time)

/**
* completedAbruptly If true, it indicates that an exception occurred in the task code
**/
private void processWorkerExit(Worker w, boolean completedAbruptly) {
       // If an exception occurs, the number of threads in the thread pool is - 1
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
        tryTerminate();

        int c = ctl.get();
        //If the current thread is running or shutdown
        if (runStateLessThan(c, STOP)) {
            //Non user task exception, that is, manually executed interrupt operation
            if (!completedAbruptly) {
                //If there are tasks to be executed in the queue, you must ensure that there is at least one thread in the thread pool, otherwise you will create a new non core thread
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

The above code can solve our previous questions

  1. How are threads in the thread pool reused? while (task != null || (task = getTask()) != null)

    If the incoming task is not empty, we will execute the incoming task. After execution, we will take the task from the queue for execution (task = getTask()). If the queue is empty, it will block and the thread will not be destroyed.

  2. If the submitted task throws an exception, will the threads in the thread pool be terminated and destroyed?

    If a task exception occurs and the original thread ends, a new thread will be started, that is, the number of threads in the thread pool will not be affected by the task exception.

4. Close the thread pool

  • shutdown() does not accept new tasks, but executes tasks in the queue

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //Modify thread pool status bit SHUTDOWN
            advanceRunState(SHUTDOWN);
            //Interrupt an idle thread. What is an idle thread?
            interruptIdleWorkers();
            //Hook method, wait for subclass implementation, default to empty method
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    

    Then let's look at the implementation of the interrupt idle thread method

    private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }
    
    private void interruptIdleWorkers(boolean onlyOne) {
            final ReentrantLock mainLock = this.mainLock;
          
            mainLock.lock();
            try {
                for (Worker w : workers) {
                    Thread t = w.thread;
                      //The key here is that the lock can be obtained, indicating that this thread is an idle thread
                    if (!t.isInterrupted() && w.tryLock()) {
                        try {
                            t.interrupt();
                        } catch (SecurityException ignore) {
                        } finally {
                            w.unlock();
                        }
                    }
                    if (onlyOne)
                        break;
                }
            } finally {
                mainLock.unlock();
            }
        }
    
    

    Here is to interrupt idle threads, so how to distinguish which are idle threads. Through w.tryLock(); A successful lock acquisition indicates that it is an idle thread, because if the thread is in a branch task, it must also obtain the lock, and the thread in blocking waiting does not obtain the lock.

    So here's the problem. Here we just interrupt and stop the idle threads. How do those non idle threads stop after performing tasks?

    After the normal thread executes the task, it will be blocked by getTask(), which contains a piece of code. When the thread pool status is SHUTDOWN and the task queue is empty, it will directly return empty, so that the thread can exit normally

     private Runnable getTask() {
         boolean timedOut = false; // Did the last poll() time out?
    
         for (;;) {
             int c = ctl.get();
             int rs = runStateOf(c);
    
              //This will jump out of the loop
             if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                 decrementWorkerCount();
                 return null;
             }
                ...
    }
    

Posted by clown[NOR] on Sun, 24 Oct 2021 09:21:39 -0700