It's so complete! This is the most detailed explanation of thread pool I have ever seen

Posted by tuuga on Tue, 01 Feb 2022 00:48:57 +0100

1. Preface

1.1 what is a thread pool?

Thread pool is a thread management technology realized by using the idea of pooling technology. It is mainly to reuse threads, conveniently manage threads and tasks, and decouple the creation of threads from the execution of tasks. We can create a thread pool to reuse the created threads to reduce the resource consumption caused by frequent thread creation and destruction. In JAVA, the ThreadPoolExecutor class is mainly used to create thread pools, and the Executors factory class is also provided in JDK to create thread pools (not recommended).

Advantages of thread pool:
Reduce resource consumption and reuse the created threads to reduce the consumption of creating and destroying threads.
Improve the response speed. When the task arrives, it can be executed immediately without waiting for the creation of the thread.
Improve the manageability of threads, and use thread pool to uniformly allocate, tune and monitor threads.

1.2 why use thread pool?

Let's recall the model of creating threads to execute tasks before there is no thread pool, as shown in the figure below

From the above, we can see some disadvantages of creating threads shown earlier:

1) Uncontrolled risk. There is no unified management for each created thread. After each thread is created, we don't know where the thread is going.
2) Each task needs to be executed by creating a new thread, which is very expensive for the system

2. Overview of java thread pool

The core implementation class of thread pool in Java is ThreadPoolExecutor, which can be used to construct a thread pool. Let's take a look at the whole inheritance system of ThreadPoolExecutor

The Executor interface provides an abstraction that decouples the execution of tasks from the creation and use of threads
The ExecutorService interface inherits from the Executor interface. On the basis of the Executor, some methods about managing the thread pool itself are added, such as viewing the status of the task, stop/terminal thread pool, obtaining the status of the thread pool, etc.

2.1 structure and composition of ThreadPoolExecutor

corePoolSize, the number of core threads, determines whether to create new threads to handle incoming tasks
maximumPoolSize, the maximum number of threads, and the maximum number of threads allowed to be created in the thread pool
keepAliveTime, the time the thread will survive when it is idle
Unit, idle survival time unit
workQueue, a task queue, is used to store submitted tasks
threadFactory, a thread factory, is used to create threads to execute tasks
handler, reject policy. When the thread pool is saturated, a certain policy is used to reject task submission

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

2.2 thread pool life cycle

2.2.1 five states of thread pool

2.2.2 thread pool life cycle flow

2.2.3 design of thread pool state in ThreadPoolExecutor

In ThreadPoolExecutor, a ctl field of AtomicInteger type is used to describe the running state and number of threads of the thread pool. The high three bits of ctl represent the five states of the thread pool, and the low 29 bits represent the number of existing threads in the thread pool. Use the least variables to reduce lock competition and improve concurrency efficiency.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Number of threads in thread pool
private static final int COUNT_BITS = Integer.SIZE - 3;
// Maximum thread capacity in thread pool
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;

// Get the running status of thread pool
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// Gets the number of valid worker threads
private static int workerCountOf(int c)  { return c & CAPACITY; }
// Number of assembly threads and thread pool status
private static int ctlOf(int rs, int wc) { return rs | wc; }

2.3 thread pool execution process

1) If workercount < corepoolsize = = > create a thread to execute the submitted task
2) If workercount > = corepoolsize & & the blocking queue is not full = = > add to the blocking queue and wait for subsequent threads to execute the submitted task
3) If workercount > = corepoolsize & & workercount < maxinumpoolsize & & blocking queue full = = > create a non core thread to execute the submitted task
4) If workercount > = maxinumpoolsize & & blocking queue full = = > execute reject policy

2.3.1 execute submit task

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
	// Number of worker threads < corepoolsize = > directly create threads to execute tasks
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	// Number of worker threads > = corepoolsize & & thread pool running = > add task to blocking queue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
		/**
         * Why do I need to double check the status of the thread pool?
         * When adding tasks to the blocking queue, the blocking queue may be full and need to wait for other tasks to move out of the queue. In this process, the state of the thread pool may change, so double check is required
         * If the state of the thread pool changes when adding a task to the blocking queue, you need to remove the task
         */
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

2.3.2 addWorker creates threads and joins thread pool

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

        // Check if queue empty only if necessary.
		// Thread pool is in non RUNNING state, adding worker failed
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
		// Judge whether the number of threads in the thread pool is within the maximum number of threads allowed in the thread pool. If threads are allowed to be created, cas updates the number of threads in the thread pool, exits the loop check, and executes the following logic of creating threads
        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
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
		// Create thread
        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 the thread pool is in the RUNNING state and the thread has been started, a thread start exception will be thrown in advance
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
					// Add threads to the created thread collection and update the largestPoolSize field used to track the number of threads in the thread pool
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
			// Start the thread to execute the task
            if (workerAdded) {
				// The starting thread will call runWorker() in the Worker to execute the task
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

2.3.3 runWorker execution task

final void runWorker(Worker w) {
	// Gets the thread executing the task
    Thread wt = Thread.currentThread();
    // Get execution task
    Runnable task = w.firstTask;
	// Empty the task in the worker
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
			// Double check whether the thread pool is stopping. If the thread pool stops and the current thread can be interrupted, the thread will be interrupted
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
				// Pre execution task hook function
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
					// Execute current 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 {
					// Post only sing task hook function
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
		// Recycle thread
        processWorkerExit(w, completedAbruptly);
    }
}

2.4 route pool rejection strategy

When the threads in the thread pool and the tasks in the blocking queue are saturated, the thread pool needs to execute the given rejection strategy to reject the submitted tasks. ThreadPoolExecutor mainly provides the following four rejection strategies to reject tasks. The default rejection policy of ThreadPoolExecutor

For AbortPolicy, an exception is thrown so that users can make specific judgments according to specific tasks

AbortPolicy,Throw RejectedExecutionException Task submission rejected abnormally
public static class AbortPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

Discard policy, do nothing and discard the task directly

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

DiscardOldestPolicy: poll the tasks in the blocking queue, and then execute the current task

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

CallerRunsPolicy, which allows the thread submitting the task to execute the task

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

3. Reasonably set thread pool parameters

  • CPU intensive tasks (N+1)

This kind of task mainly consumes CPU resources. The number of threads can be set to N (number of CPU cores) + 1. One thread more than the number of CPU cores is to prevent accidental page interruption of threads or the impact of task suspension caused by other reasons. Once the task is suspended, the CPU will be idle, and in this case, an extra thread can make full use of the idle time of the CPU.

  • I/O intensive tasks (2N)

When this task is applied, the system will spend most of the time dealing with I/O interaction, and the thread will not occupy the CPU for processing during the time period of dealing with I/O. at this time, the CPU can be handed over to other threads for use. Therefore, in the application of I/O-Intensive tasks, we can configure more threads. The specific calculation method is 2N. How to judge whether it is CPU intensive task or IO intensive task?

The simple understanding of CPU intensive is the task of using CPU computing power, such as sorting a large amount of data in memory. When it comes to network reading and file reading, they are IO intensive. The characteristic of these tasks is that CPU computing takes less time than waiting for the completion of IO operations, and most of the time is spent waiting for the completion of IO operations.

4. Some suggestions on using thread pool

[mandatory] thread pools are not allowed to be created using Executors, but through ThreadPoolExecutor. This processing method makes the writing students more clear about the running rules of thread pools and avoid the risk of resource depletion

Note: the disadvantages of thread pool object returned by Executors are as follows: 1) Executors Fixedthreadpool (fixedpoolsize) and SingleThreadPool: the allowable request queue length is integer MAX_ Value, which may accumulate a large number of requests, resulting in OOM. 2) Excutors. The number of threads allowed to be created by cachedthreadpool() is integer MAX_ Value, a large number of threads may be created, resulting in OOM.

[suggestion] when creating a thread pool, try to prefix the thread with a specific business name to facilitate the location of the problem

// 1\.  Using guava's ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();

// 2\.  Implement the ThreadFactory interface yourself

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * Thread factory, which sets the thread name, which is helpful for us to locate the problem.
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final ThreadFactory delegate;
    private final String name;

    /**
     * Create a thread pool production factory with a name
     */
    public NamingThreadFactory(ThreadFactory delegate, String name) {
        this.delegate = delegate;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = delegate.newThread(r);
        t.setName(name + threadNum.incrementAndGet());
        return t;
    }

}

[suggestion] try to use different thread pools for different types of business tasks

5. Other issues?

  • How do thread pools reuse threads that have been created?

A Work object in the thread pool can be regarded as a thread. If the number of threads in the thread pool has reached the maximum, the threads in Woker can be reused, that is, continuously cycle to obtain tasks from the queue and then execute the tasks. If the tasks obtained from the blocking queue are not null,

In this way, threads can be reused to perform tasks,

  • Will the core worker thread be recycled?

summary

This interview question contains almost all the interview questions and answers he encountered in a year, even including the detailed dialogue and quotations in the interview. It can be said that the details are to the extreme, and even the optimization of resume and how to submit resume are easier to get interview opportunities! It also includes teaching you how to get the push quota of some big factories, such as Alibaba and Tencent!

A celebrity said that success depends on 99% sweat and 1% opportunity, and if you want to get that 1% opportunity, you have to pay 99% sweat first! Only if you persevere towards your goal step by step can you have a chance to succeed!

Success will only be left to those who are prepared! Free access to information: stamp here

Reuse the thread in Woker, that is, continuously cycle to get the task from the queue and then execute the task. If the task obtained from the blocking queue is not null,

In this way, threads can be reused to perform tasks,

  • Will the core worker thread be recycled?

summary

This interview question contains almost all the interview questions and answers he encountered in a year, even including the detailed dialogue and quotations in the interview. It can be said that the details are to the extreme, and even the optimization of resume and how to submit resume are easier to get interview opportunities! It also includes teaching you how to get the push quota of some big factories, such as Alibaba and Tencent!

A celebrity said that success depends on 99% sweat and 1% opportunity, and if you want to get that 1% opportunity, you have to pay 99% sweat first! Only if you persevere towards your goal step by step can you have a chance to succeed!

Success will only be left to those who are prepared! Free access to information: stamp here

Topics: Java Interview Programmer