ThreadPoolExecutor thread reuse the underlying source code, understand?

The more you know, the more you don't know!

Everyone must know about the thread pool. The knowledge points that must be mastered include: the input parameter list in the constructor, what kinds of thread pools are common and usage scenarios, why to use the thread pool, the processing flow after the thread pool receives a task, and so on.

This paper does not explain the above problems, but mainly wants to understand the underlying principle of "thread reuse".

Long winded: look at the source code. Don't knock every line of code as soon as you come up (I believe it won't last for a few minutes). First master the general idea, and then elaborate.

1. Overall idea

I believe the following picture is familiar to everyone:

In fact, it is the processing rule for the thread pool to accept tasks. Where did the rules in the picture come from? Nonsense, of course, it was drawn by the big guys looking at the source code! The boss is not me!!!
Well, actually, "thread reuse" has a lot to do with the task queue in the above figure.

Thread reuse is not a simple idea of new Thread and then thread.start().
Instead, the task is decoupled from the thread. The task is handed over to the queue for storage and management, and then the thread continuously takes the task from the queue and then executes the task (Runnable Implementation). In fact, the execution process is to directly call the run method and treat the run method as a simple method.

2. Learning source code

The idea has been exposed above. Next, just follow the source code verification~

Let's go directly to the ThreadPoolExecutor#executor method. After all, this method is the way we submit tasks every day. Please pay attention to the key points!!!

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //If the number of threads is less than the number of core threads, directly create a new thread and submit the task
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        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);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

Let's first look at the code when the number of threads is less than the number of core threads:

  if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

Here, the workerCountOf method is to obtain the number of threads in the pool. Later, you will know that a Worker instance corresponds to a Thread. Do you suddenly think it makes sense for others to obtain this method name.

Continue to focus on the key points and follow addWorker. First, let's talk about the next two input parameters: the first input parameter is each task written in the business code, nothing to say; Second parameter: true - whether the current number of threads is less than the number of core threads. If less than, new threads will be added; if greater than, no new threads will be added; false - whether the current number of threads is less than the maximum number of threads.

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

            // Check if queue empty only if necessary.
            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;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        
//---------------I am the dividing line---------------------------------

        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;
    }

It is recommended to split this code into upper and lower parts with "I am a dividing line":

  • (1) Thread count judgment:
    It is mainly to obtain the latest number of threads, and then compare it with corePoolSize or maxPoolSize. If it is less than, continue the following code to add new threads;
  • (2) New thread Worker
  Worker w = null;
        try {
        //New Worker
            w = new Worker(firstTask);

//The construction method of worker is as follows:
       Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker  
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

It can be seen that when the Worker is instantiated, it mainly does two things:
(1) Save the task in the firstTask variable;
(2) The internal variable thread is instantiated. Take a closer look at the following codes:

 this.thread = getThreadFactory().newThread(this);

Notice this??? Does that mean that the Worker must be implement Runnable?

Well, you must take a good look at the rewritten run method.

Next, you will find that ThreadPoolExecutor#runWorker is called directly

In that case, continue to see what runWoker did?

 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //Get submitted tasks
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        //Key point: loop. Either take the task of new Worker or the task returned by getTask
            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 {
                       // Key points: implementation of tasks
                        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);
        }
    }

In fact, that's it: we can see that the Worker re run s, which is actually a cycle, constantly taking out tasks and then executing tasks.

Then continue to look at the getTask method and see where to get the task continuously > > > > > > >

It's amazing. It's really from the queue. Normally, if the thread pool of the submitted task is full, it should enter this queue.

In fact, we still need to explore a problem here. Since the Worker implements Runnable, when will the Worker#run be called

In fact, the above code has been posted after addworker - > new worker.

Tags: Java

Posted on Sat, 02 Oct 2021 17:55:49 -0400 by hammerslane