Exploration of ThreadPool Executor Principle in Java

Keywords: less Mobile

Thread pool mainly solves two problems: on the one hand, thread pool can provide better performance when a large number of asynchronous tasks are executed, because using thread pool can reduce the call overhead of each task (because thread pool is reusable). On the other hand, thread pools provide a means of resource limitation and management, such as thread management when performing a series of tasks. Each ThreadPool Executor also retains some basic statistics, such as the number of tasks completed by the current thread pool.
In addition, thread pools provide many adjustable parameters and scalable hooks. Programmers can use it more conveniently
Factory methods such as new Cached ThreadPool (unlimited thread pool, thread automatic recycling), new Fixed ThreadPool (fixed size thread pool) new Single ThreadExecutor (single thread), of course, users can also customize.

Class graph structure


image.png

Executors is actually a tool class, which provides many static methods to return different thread pool instances according to the user's choice.
ThreadPoolExecutor inherits AbstractExecutor Service. The member variable ctl is an Integer atomic variable that records the state of the thread pool and the number of threads in the thread pool, similar to ReentrantReadWriteLock, which uses one variable to store two kinds of information.
The Integer type is a 32-bit binary notation, where the top three bits are used to represent the state of the thread pool, and the last 29 bits are used to record the number of threads in the thread pool.

//Used to mark thread pool status (3 bits high), number of threads (29 bits low)
//The default is RUNNING status with 0 threads
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

//Thread Number Mask Number
private static final int COUNT_BITS = Integer.SIZE - 3;

//Maximum number of threads (low 29 bits) 00011111111111111111111111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

//(Top 3): 11100000000000000000000000000000000000
private static final int RUNNING    = -1 << COUNT_BITS;

//(Top 3): 00000000000000000000000000000000000000
private static final int SHUTDOWN   =  0 << COUNT_BITS;

//(Top 3): 00,000,000,000,000,000,000,000,000,000,000,000,000
private static final int STOP       =  1 << COUNT_BITS;

//(Top 3): 01000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000
private static final int TIDYING    =  2 << COUNT_BITS;

//(Top 3): 01100000000000000000000000000000000
private static final int TERMINATED =  3 << COUNT_BITS;

// Obtain high three running status
private static int runStateOf(int c)     { return c & ~CAPACITY; }

//Get the number of low 29-bit threads
private static int workerCountOf(int c)  { return c & CAPACITY; }

//Calculate new ctl values, thread status and number of threads
private static int ctlOf(int rs, int wc) { return rs | wc; }

Thread pool status implications:

  • RUNNING: Accept new tasks and handle tasks in blocking queues
  • SHUTDOWN: Deny new tasks but handle tasks in blocked queues
  • STOP: Rejecting new tasks and discarding tasks in the blocking queue interrupts the tasks being processed
  • TIDYING: All tasks have been executed (including tasks in the blocking queue). The current thread pool active thread is 0, and the terminated method will be called.
  • TERMINATED: Termination status. The state of terminated method calls after completion

Thread pool state transition:

  • RUNNING -> SHUTDOWN
    An explicit call to the shutdown() method, or an implicit call to finalize(), in which the shutdown() method is called.
  • RUNNING or SHUTDOWN)-> STOP
    Explicit shutdownNow() method
  • SHUTDOWN -> TIDYING
    When the thread pool and task queue are empty
  • STOP -> TIDYING
    When the thread pool is empty
  • TIDYING -> TERMINATED
    When the terminated() hook method is completed

Thread pool parameters:

  • corePoolSize: Number of Core Threads in Thread Pool
  • workQueue: A blocking queue used to save tasks awaiting execution. For example, bounded Array BlockingQueue based on array, unbounded Linked BlockingQueue based on linked list, synchronous Queue with at most one element, Priority BlockingQueue based on priority queue, can be used for reference. https://www.atatech.org/articles/81568

  • Maximun PoolSize: Maximum number of threads in the thread pool.

  • ThreadFactory: Factory for creating threads

  • Rejected Execution Handler: Saturation policy, policies taken when the queue is full and the number of threads reaches maximum unPoolSize, such as AbortPolicy (throwing an exception), Caller RunsPolicy (using the caller's thread to run the task), Discard Oldest Policy (calling poll to discard a task, executing the current task), DiscardPolicy (discarding silently, discarding no exception)
  • Keey AliveTime: Survival time. If the number of threads in the current thread pool is larger than the number of core threads and is idle, the maximum time that these idle threads can survive
    Time Unit, unit of time for survival

Thread pool type:

  • newFixedThreadPool
    Create a thread pool with nThreads as the number of core threads and the maximum number of threads, and the length of the blocking queue is Integer.MAX_VALUE. keeyAliveTime=0 indicates that as long as the number of threads is larger than the number of core threads and the current idle thread is recycled.
  public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
  }
//Create factories with custom threads
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory);
  }
  • newSingleThreadExecutor
    Create a pool of threads with 1 core threads and 1 maximum threads, and the length of the blocking queue is Integer.MAX_VALUE. Keey AliveTime = 0 indicates that as long as the number of threads is larger than the number of core threads and the current idle thread is recycled.
  public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
  }

  //Use your own thread factory
  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory));
  }
  • newCachedThreadPool
    Create a pool of threads that create threads on demand. The initial number of threads is 0, the maximum number of threads is Integer.MAX_VALUE, and the blocking queue is listed as a synchronous queue. Keey AliveTime = 60 indicates that the current thread will be recycled as long as it is idle within 60 seconds. This particular feature is that tasks added to the synchronization queue will be executed immediately. There is at most one task in the synchronization queue, and it will be executed immediately after it exists.
 public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
  }

  //Using a custom thread factory
  public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>(),
                                    threadFactory);
  }
  • newSingleThreadScheduledExecutor
    Create a minimum number of threads corePoolSize of 1, a maximum of Integer.MAX_VALUE, and a blocking queue as the thread pool of Delayed WorkQueue.
  public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
      return new DelegatedScheduledExecutorService
          (new ScheduledThreadPoolExecutor(1));
  }
  • newScheduledThreadPool
    Create a minimum number of threads corePoolSize, maximum Integer.MAX_VALUE, and a blocking queue as the thread pool of Delayed WorkQueue.
  public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
      return new ScheduledThreadPoolExecutor(corePoolSize);
  }

Worker inherits AQS and Runnable as specific objects to carry tasks. Worker inherits AQS and implements simple non-reentrant exclusive locks, in which status=0 indicates that the locks are not acquired, that is, unlocked, and state=1 indicates that the locks have been acquired, that is, locked.

DefaultThreadFactory is a thread factory. The newThread method is a grouping package for threads, in which poolNumber is a static atomic variable to count the number of thread factories, and threadNumber is used to record how many threads are created by each thread factory.

Source code analysis

Adding tasks to thread pool exectue method

public void execute(Runnable command) {

    if (command == null)
        throw new NullPointerException();

    //Get the status of the current thread pool + the number of threads variable
    int c = ctl.get();

    //Whether the number of threads in the current thread pool is less than corePoolSize or less opens a new thread to run
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    //If the thread pool is in RUNNING state, add tasks to the blocking queue
    if (isRunning(c) && workQueue.offer(command)) {

        //Second Inspection
        int recheck = ctl.get();
        //If the current thread pool state is not RUNNING, delete tasks from the queue and execute a rejection policy
        if (! isRunning(recheck) && remove(command))
            reject(command);

        //If the current thread pool thread is empty, add a thread
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //If the queue is full, new threads are added and new failures are rejected
    else if (!addWorker(command, false))
        reject(command);
}
  • If the number of threads in the current thread pool is less than corePoolSize, a new thread is opened
  • Otherwise, add tasks to the task queue
  • If the task queue is full, try to start a new thread to execute the task, and if the number of threads > maximumPoolSize, execute the rejection policy.

Focus on the addWorkder method:

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

        // Check if the queue is empty only if necessary. (1)
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        //Loop cas increases the number of threads
        for (;;) {
            int wc = workerCountOf(c);

            //Returns false if the number of threads exceeds the limit
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //cas increases the number of threads while only one thread succeeds
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //If CAS fails, it depends on whether the state of the thread pool has changed, and if the change jumps to the outer loop to retry the state of the thread pool, or the inner loop to retry the cas.
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    //This shows that cas has succeeded. (2)
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //Create worker
        final ReentrantLock mainLock = this.mainLock;
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {

            //Exclusive locks are added to synchronize workers because multiple threads may call the execute method of the thread pool.
            mainLock.lock();
            try {

                //Re-check thread pool status to avoid calling shutdown interface before acquiring locks (3)
                int c = ctl.get();
                int rs = runStateOf(c);

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //Adding tasks
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            //Adding success starts the task
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

The code is relatively long, mainly divided into two parts. The first part of the double loop is to increase the number of thread pool threads through cas. The second part is to add tasks to workers safely and start task execution.

Look at the first part (1)

rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())

Open! After operation, it is equivalent to

s >= SHUTDOWN && (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty())

That is to say, false is returned in the following cases:

  • The current thread pool status is STOP, TIDYING, TERMINATED
  • The current thread pool state is SHUTDOWN and has the first task
  • The current thread pool status is SHUTDOWN and the task queue is empty

The inner loop uses cas to increase the number of threads. If the number of threads exceeds the limit, it returns false. If cas succeeds, cas exits the double loop. If cas fails, it depends on whether the current state of thread pool has changed. If it changes, it re-enters the outer loop to retrieve the state of thread pool. If not, it enters the inner loop to continue cas attempts.

In the second part, CAS is successful, that is to say, the number of threads has been increased by one, but now the task has not started to execute. We use global exclusive locks to control adding tasks in workers. In fact, we can also use concurrent secure set, but the performance is not as good as exclusive locks (as we know from the comments). It is important to re-check the state of the thread pool after acquiring the lock, because other threads may change the state of the thread pool before acquiring the lock in this method, such as calling the shutdown method. Successful addition initiates task execution.

Execution of Workthread Worker

Let's first look at the constructor:

Worker(Runnable firstTask) {
    setState(-1); // Prohibit interruption before calling runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);//Create a thread
}

A new state-1 is added here to avoid interruption of the current thread worker thread, such as calling shutdownNow of the thread pool. If the current worker state >= 0, the interruption flag of the thread will be set. Here we have set - 1, so the thread will not be interrupted if the condition is not satisfied. When running runWorker, the unlock method is called, which changes status to 0, so calling shutdownNow interrupts the worker thread.

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // status is set to 0, allowing interruption
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {

                w.lock();
                // If the current state of the thread pool is at least stop, an interrupt flag is set.
                // If the current state of the thread pool is RUNNIN, the interrupt flag is reset and needs to be reset after resetting.
                //Check the state of the thread pool because the shutdown method of the thread pool may be called when the interrupt flag is reset
                //Changed thread pool status.
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();

                try {
                    //Do something before the task is executed
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();//Execution of tasks
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        //Do something after the task is completed
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    //Statistics of how many tasks the current worker has accomplished
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {

            //Clear execution
            processWorkerExit(w, completedAbruptly);
        }
    }

If the current task is empty, it is executed directly. Otherwise, getTask is called to get a task execution from the task queue. If the task queue is empty, the worker exits.

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Reduce the number of worker threads if the current thread pool state >= STOP or the thread pool state is shutdown and the workforce is null
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        boolean timed;      // Are workers subject to culling?

        for (;;) {
            int wc = workerCountOf(c);
            timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if (wc <= maximumPoolSize && ! (timedOut && timed))
                break;
            if (compareAndDecrementWorkerCount(c))
                return null;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }

        try {

            //Select call poll or blocked take based on timed
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

    //Statistics the number of tasks completed by the entire thread pool
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    //Attempt to set the thread pool status to TERMINATED if the current shutdown status and the task force is empty
    //Or the current stop state. There are no active threads in the current thread pool
    tryTerminate();

    //If the current number of threads is less than the number of core threads, increase
    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

shutdown operation

When shutdown is invoked, the thread pool will not accept new tasks, but the tasks in the work queue will be executed, but the method returns immediately without waiting for the queue task to complete.

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //Privilege check
        checkShutdownAccess();

        //Set the current thread pool status to SHUTDOWN and return directly if it is already SHUTDOWN
        advanceRunState(SHUTDOWN);

        //Setting interruption flag
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    //Trial state changed to TERMINATED
    tryTerminate();
}

//If the current state >= targetState, return directly, or set the current state to targetState.
private void advanceRunState(int targetState) {
    for (;;) {
        int c = ctl.get();
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

//Set the interrupt flag for all threads. The main reason is that the global lock is added first. At the same time, only one thread can set the interrupt flag when it calls shutdown, then try to get the worker's own lock, and set the interrupt flag if it succeeds.
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

shutdownNow operation

When shutdown is invoked, the thread pool will no longer accept new tasks and discard the tasks in the work queue. The tasks being executed will be interrupted, but the method returns immediately without waiting for the activated tasks to be executed. Return to the task list in the queue.

Call queue drainTo An element of the current queue arrives at taskList,
//It may fail. If the queue sea is not empty after drainTo is called, it is deleted circularly and added to taskList.
public List<Runnable> shutdownNow() {

    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();//Privilege check
        advanceRunState(STOP);// Set the thread pool state to stop
        interruptWorkers();//Interrupt thread
        tasks = drainQueue();//Mobile Queue Tasks to tasks
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

//Call drainTo of the queue once the elements of the current queue go to taskList,
//It may fail. If the queue sea is not empty after drainTo is called, it is deleted circularly and added to taskList.
private List<Runnable> drainQueue() {
    BlockingQueue<Runnable> q = workQueue;
    List<Runnable> taskList = new ArrayList<Runnable>();
    q.drainTo(taskList);
    if (!q.isEmpty()) {
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    return taskList;
}

awaitTermination operation

Waiting for the thread pool state to change to TERMINATED returns, or time out. Because of the exclusive lock of the whole process, shutdown or shutdownNow is usually used after calling.

    public boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (;;) {
                if (runStateAtLeast(ctl.get(), TERMINATED))
                    return true;
                if (nanos <= 0)
                    return false;
                nanos = termination.awaitNanos(nanos);
            }
        } finally {
            mainLock.unlock();
        }
    }

summary

Thread pool ingeniously uses an Integer type atomic variable to record the state of thread pool and the number of threads in thread pool. The design considers that the future (2 ^ 29) - 1 thread may not be enough. At that time, it only needs to change atomic variable into Long type, and then the mask number changes. But why not define Long once and for all now, mainly considering the use of int type operation? It works faster than the Long type.

Each worker thread can handle multiple tasks by controlling the execution of tasks through the state of thread pool. Thread pool reduces the overhead of thread creation and destruction through thread reuse, and avoids the overhead of thread scheduling and thread context switching by using task queue.

It is also important to note that the function of calling shutdown method is only to modify the state of thread pool to make the current task fail and interrupt the current thread. This interrupt does not terminate the running thread, but only sets the interrupt flag of the thread. If there is no interrupt flag in the thread to do something, then this has no effect on the thread.

Posted by bapi on Sat, 15 Jun 2019 18:06:04 -0700