Resolution of java thread pool principle

During May holidays, Daxiong read a Book "The Art of java Concurrent Programming" and understood the basic workflow of thread pool. He unexpectedly found that the working principle of thread pool is very similar to that of Internet companies.

Thread pool process

Principle Analysis

The relationship between Internet companies and thread pools

Here is a metaphor to describe the thread pool. There are some nouns in it that you may not be sure about. The source parsing section at the back will cover them.

You can think of a thread pool as a R&D department with many programmers who work in a large office (HashSet workers).Programmers'incomplete needs (Runnable/Callable) are queued in the workQueue.Each R&D department is configured with a number of core programmers (corePoolSize) and a maximum number of programmers (maximumPoolSize).The specific task to be done is the requirements of the product.

A new thread pool is equivalent to creating a R&D department, which needs to specify the number of backbone programmers, the maximum number of programmers that can be accommodated, what kind of BlockingQueue to use for the requirement pool, how to reply to the product (rejection policy) if the demand is too busy, and so on.At first there was no programmer in this R&D department.

When a product raises a demand for this R&D Department (certainly not just one, they will keep increasing demand).Here's an example of a requirement)

First you'll see if the backbone programmers are fully recruited.

If not full, a key programmer will be recruited, let him keep working (brutal). After finishing the task just sent, he will actively find the next requirement in the demand pool to do (good employee). If there is no requirement in the demand pool, he will stop working, and then the R&D department will cut him off. If the number of key programmers is not enough after cutting off, it willRecruit another programmer.If the number of key programmers is enough, you won't be hired.

If the number of backbone programmers is full, it depends on whether the demand pool is full or not. If the demand pool is not full, throw the demand into the demand pool. If the demand pool is full, it depends on whether the number of programmers has reached the maximum. If it has been reached, we can't do this and have no resources for the product. If not, recruit a programmer and let him keep working.If the demand pool has no more tasks, he stops working and the R&D department will cut him off. If the number of key programmers is not enough, he will recruit another programmer.If the number of key programmers is enough, you won't be hired.

Source Parsing

First is worker (programmer)

Worker is installed inside a HashSet (workers), which he uses to perform tasks. Their responsibility is to constantly take tasks from the workQueue and then execute them.When a task is not available in the workQueue or the thread pool reaches a specific state, the worker is removed from the workers.

Below is the Worker source, removing the non-critical stuff

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{

    // Identify which thread this task is running on
    final Thread thread;
    Runnable firstTask;
    // Several tasks completed
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        // Block interrupts until runWorker executes
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // Get a thread directly from the thread factory you provide
        this.thread = getThreadFactory().newThread(this);
    }

    // Call the runWorker method inside ThreadPoolExecutor
    public void run() {
        runWorker(this);
    }

    // Here's what AQS is all about

    // 0 means no lock
    // 1 means locked
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

Worker implements the Runnable interface, so he is a task, has the run method, and inherits AQS, so he is also a lock.

Below is the process of submitting the task

The submit task has submit and execute, submit is to wrap Callable or Runnable into FutureTask first, then call execute, so the core is to analyze execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // There are two pieces of information in this c, one is how many worker s are present, and the other is what the status of the current thread pool is.
    // The workerCountOf method extracts the number of workers from the inside
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) { // The number of current worker s is less than the number of core threads required
        // Add worker to execute and add success is done, that is, as long as there are fewer workers than core threads, a worker will be created
        // Whether the core thread is working or not, or whether the workQueue is full
        // The second parameter of addWorker indicates whether core threads (or core workers) are required
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // The current worker has reached or exceeded the number of core threads or has failed to add the worker before going down the process
    // worker already has more threads than core

    // If the thread pool does not have shutdown 
    // Try adding tasks to the workQueue and move in if the team is successful in joining the team
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            // Check status again If the thread pool is going to stop, then reject the task and throw the worker out of the work queue
            reject(command);
        else if (workerCountOf(recheck) == 0)
            // Add a worker if you don't have a worker (that means you didn't add one, and I didn't expect it to happen in this scenario)
            addWorker(null, false);
        // Otherwise, leaving it on the work queue doesn't matter and waiting for the worker to handle it
    }
    // If the queue filling fails or the thread pool state is not satisfied, try adding a normal worker (non-core thread)
    else if (!addWorker(command, false))
        // Reject tasks if failures are added
        // One failure may be that the number of worker s has reached the maximum PoolSize you gave
        // On the other hand, it may be that the state of the thread pool has been checked incorrectly
        reject(command);
}

You can see that the execute method completes the process described in the figure above, Thread Pool Processing.There are several questions for Daxiong to see here, one is how Woker created and joined workers, one is how worker started, and the other is how worker works.

Life goes on

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

        // Do some checks to ensure that the state of the thread pool meets certain criteria
        // And you have to submit a task, and workQueue can't be empty
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            // See if you want to create a core worker or a regular worker
            // Core view does not exceed corePoolSize, and normal view does not exceed maximumPoolSize
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
            // Increasing the number of worker s is a failure
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                // Intermediate thread pool state changed
                continue retry;
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // This is how worker was created
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            // Adding worker is to add a global lock
            mainLock.lock();
            try {
                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) {
                // The worker was started here
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

This code solves the problem of how Woker created and joined workers and how the worker started.

The core work addWorker does is create a worker, start a worker, and do some validation before creating it.After calling the start of the thread inside the worker, wait for the cpu to schedule the run method of the worker to execute.

public void run() {
    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 {
        // Task is the task that creates the worker and takes it from within the workQueue
        // Jump out if not
        while (task != null || (task = getTask()) != null) {
            w.lock();   // Lock first. If not, several threads may submit tasks at the same time, causing some sharing state problems

            // Do some status checks
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // Call beforeExecute before executing the task, which is empty by default
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // Unlike Runnable, which we usually understand, you can understand that his run is an ordinary method
                    // He calls run directly to perform the task. The thread's start just runs the run inside the worker
                    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 {
                    // After the adjustment, you can get the exception inside
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // Jump out of while to indicate that there are no tasks to perform
        processWorkerExit(w, completedAbruptly);
    }
}

It's also easier to keep taking tasks from the workQueue and executing them until there are no tasks to jump out of.Next comes the question of how the worker was destroyed

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

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

    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            // If there are more threads than the core, remove the Worker directly after execution
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // Less than the number of core threads will add a Worker to keep him waiting for tasks to be received (hiring)
        addWorker(null, false);
    }
}

Remove the worker directly from the workers, and if there are fewer workers than the core threads, add another worker or not.

Some Experiences

Looking at the source code, don't get too entangled with details. Like this thread pool, I see a lot of articles on the Internet calculating decimal numbers of those bit operations. I feel that it is a waste of time and I don't catch the focus.

Of course, this is not absolute (seemingly contradictory), and some details are still very subtle and worth learning.Or this bit operation, why only one int is used to represent the state of the thread pool and the number of worker s?

To associate more, or to perform this bit operation, is he very similar to a read-write lock in that an int represents both the write state and the read state.Can Worker inherit AQS to remind you of all kinds of AQS?

In short, the first time you look at it, you must not indulge in details. It will make you lost and lose confidence. The second and third time you can focus on details and feel the beauty of master design.Of course, the author just looked at it once (escape ~)

Last

During the May 15 holidays, Daxiong read the book "Java Concurrent Programming Art", sorted out a gitbook note (not yet finished), required students can scan the end of the text QR code to focus on the public number "Daxiong and you learn programming together", back-stage reply I love java to pick up.This gitbook is not completely finished yet, so there may be some minor errors.One of these articles will be available approximately every two days in the future.

Here is a catalog shot of this gitbook

Tags: Java Programming less

Posted on Fri, 08 May 2020 13:12:06 -0400 by cmzone