Java thread pool source code analysis

Posted by PugJr on Sun, 10 Oct 2021 05:49:18 +0200

Source code analysis of Java thread pool ThreadPoolExecutor (I)

JDK provides a simple way to create thread pools. Different types of thread pools can be created through the API provided by Executors, such as

		// Create a single thread pool;
		ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            System.out.println("TEST EXECUTOR");
        });

1, Here we need to introduce a few questions

  1. What constitutes a thread pool?
  2. How do threads in the thread pool work?

2, Construction of thread pool

2.1 thread pool packaging

When you click Executors.newSingleThreadExecutor(), you can see the following code

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

The above code is to build a ThreadPoolExecutor and pass the constructed ThreadPoolExecutor as a parameter into the FinalizableDelegatedExecutorService object. Here, the decorator mode is applied. The ThreadPoolExecutor is only wrapped through the FinalizableDelegatedExecutorService. The code is as follows

	static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
        FinalizableDelegatedExecutorService(ExecutorService executor) {
            super(executor);
        }
        protected void finalize() {
            super.shutdown();
        }
    }

In fact, FinalizableDelegatedExecutorService only passes ThreadPoolExecutor to DelegatedExecutorService. In addition, it only rewrites the Object.finalize method to call ExecutorService.shutdown, and DelegatedExecutorService only wraps ThreadPoolExecutor to expose the methods in ExecutorService (exposes only the ExecutorService methods). When the ExecutorService method is executed, the ThreadPoolExecutor method is actually executed. The code is as follows

	 /**
     * A wrapper class that exposes only the ExecutorService methods
     * of an ExecutorService implementation.
     */
	static class DelegatedExecutorService extends AbstractExecutorService {
        private final ExecutorService e;
        DelegatedExecutorService(ExecutorService executor) { e = executor; }
        public void execute(Runnable command) { e.execute(command); }
        public void shutdown() { e.shutdown(); }
        // Omit N multiple codes
    }

OK, so far, in fact, we have made it clear that when the ExecutorService built through Executors.newSingleThreadExecutor is actually to build ThreadPoolExecutor, we will analyze how ThreadPoolExecutor is built and the meaning of construction parameters

2.2. ThreadPoolExecutor construction parameters

From Executors.newSingleThreadExecutor down, you can see that the ThreadPoolExecutor construction method is as follows

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,BlockingQueue
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

The meanings of several parameters involved here are as follows

  1. corePoolSize: the number of core threads. Threads in the thread pool are divided into core threads and non core threads
  2. maximumPoolSize: the maximum number of threads in the thread pool. The thread pool cannot create threads without control. Here it plays a limiting role
  3. keepAliveTime: value of idle thread holding time, used in combination with TimeUnit
  4. TimeUnit: time unit, used in combination with keepAliveTime
  5. BlockingQueue: task queue. When a general thread pool executes a task, if the number of tasks is greater than the number of threads, it will put the task into the queue for waiting
  6. ThreadFactory: factory for producing threads. Thread pool is used to build threads and work through ThreadFactory
  7. RejectedExecutionHandler: reject policy. When the thread pool fails to execute tasks, it will follow the corresponding reject policy. By default, JDK provides four kinds, namely (AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy and DiscardPolicy). The specific functions will be described in detail later

2.3. BlockingQueue in ThreadPoolExecutor

Similarly, in the Executors.newSingleThreadExecutor code, it can be seen that the LinkedBlockingQueue is used as the task queue in the ThreadPoolExecutor, which is a thread safe container based on AQS mechanism. The internal storage structure adopts one-way linked list and follows FIFO. The implementation mechanism will be explained later. Here is a brief description of the methods that will be applied in practical use

  1. BlockingQueue.offer: put elements into the queue, and put tasks (Runnable) into the thread pool
  2. BlockingQueue.take: get elements from the queue. If there are no elements in the queue, the thread will be blocked until there are elements in the queue
  3. BlockingQueue.poll: the same as take, but poll sets the timeout mechanism. If the element cannot be obtained from the queue within the specified time, NULL will be returned

The above is the implementation of the offer, take and poll methods in the BlockingQueue by LinkedBlockingQueue. Due to the large content, the source code of LinkedBlockingQueue will be analyzed separately later

2.4. ThreadFactory in ThreadPoolExecutor

In the ThreadPoolExecutor construction method, if the corresponding ThreadFactory is not specified, a simple thread factory (DefaultThreadFactory) will be built with Executors.defaultThreadFactory by default. The code is as follows

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

A name is attached to the built thread in the newThread, and the thread is still created in the way of new Thread
The construction of thread pool is almost over here. Some details have not been mentioned yet. It is suggested that interested friends can go to the source code and go through the construction of thread pool

3, How thread pools work

3.1 difference between submit and execute

When tasks need to be executed through the thread pool, you can usually choose to call submit and execute. The difference between the two is that submit can pass in Callable or Runnable, while Runnable is passed in execute. In fact, submit only encapsulates Callable or Runnable, and finally calls execute. The code is as follows

    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(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

It can be seen that no matter whether the submit is Callable or Runnable, it will eventually be encapsulated into a FutureTask through newTaskFor, call execute and return to the caller. Once the caller executes FutureTask.get, he will wait for the result to return

3.2. Detailed explanation of execute method

Generally, the thread pool will go through the following judgments when executing tasks through execute

  1. Judge whether the number of threads in the thread pool is less than the number of core threads. If it is less than, start the core thread to run the task
  2. If condition 1 is not satisfied, try to put the task into the queue. If the task is put into the queue successfully, it will be rechecked. The logic is as follows
    2.1. After the task is placed successfully, the status of the thread pool will be verified again. If the thread pool is not in the RUNNING state, that is, the thread pool is closed during the placement of the task, it will try to remove the task from the queue and adopt the reject policy
    2.2. If the judgment in 2.1 fails and the number of rechecked threads is equal to 0, start a non core thread to obtain and execute tasks in the task queue
  3. If conditions 1 and 2 are not satisfied, i.e. not less than the number of core threads, the queue cannot accommodate new elements. At this time, a non core thread will be tried to execute the task. If the execution fails, the preset rejection policy will be rejected through reject;
 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
  		int c = ctl.get();
  		// Obtain the current number of threads through workerCountOf to judge whether the current number of threads is less than the number of core threads. If it is less than the number of core threads, call addWorker to execute the task;
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // If the number of threads in the thread pool is no less than the number of core threads, it will try to put the task into the queue. The success of putting the task depends on whether the offer is executed successfully, that is, the queue still has space to accommodate elements;
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // If the above two judgments cannot pass, that is, the number of core threads is not less than, and the queue cannot accommodate new elements, then an attempt will be made to start a non core thread to execute the task. If the execution fails, the preset rejection policy will be taken through reject;
        else if (!addWorker(command, false))
            reject(command);
}

3.3. addWorker method

It can be seen from execute that the thread pool will execute the task through the addWorker method. The addWorker method will encapsulate the task, encapsulate the Runnable as a Worker, and put the Worker into the thread collection workers. If the placement is successful, execute Thread.start to start the thread to execute the task. In fact, the run method of the Worker is executed. Some key codes are as follows

private final HashSet<Worker> workers = new HashSet<Worker>();

private boolean addWorker(Runnable firstTask, boolean core) {
	// Some codes are omitted here
	 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 {
                 // 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) {
                 t.start();
                 workerStarted = true;
             }
         }
     } finally {
         if (! workerStarted)
             addWorkerFailed(w);
     }
     return workerStarted;
}

The Worker structure is as follows

  1. Get the thread through ThreadFactory and assign it to the thread property
  2. Implement Runnable, rewrite the run method, and call the internal runWorker
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    	// Omit part of the code
        final Thread thread;
        Runnable firstTask;
        volatile long completedTasks;
        
        Worker(Runnable firstTask) {
            setState(-1); 
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        
        public void run() {
            runWorker(this);
        }
    }

3.4 runWorker lets threads execute tasks repeatedly

The principle of runWorker executing tasks is as follows

  1. A while loop will be opened inside the runWorker method to continuously obtain tasks from the task queue through the getTask method
  2. If there are still tasks in the queue, execute the beforeExecute method, which is an extension method and has no processing by default
  3. Execute task.run. The task object is the Runnable that was put into the queue at the beginning
  4. Execute after execute in finally, which is the same as before execute by default
  5. Count the completedTasks of the Worker and record the number of tasks executed by the Worker
  6. When the task queue is empty, execute processWorkerExit to clear the thread
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        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 {
                        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 {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

Topics: Java Interview