Threads are a mountain that every Java developer can’t get around. Thread pools are used the most at work and asked the most in interviews.

However, many people do not know the core principles and logic of thread pool, and even do not know how to configure thread pool parameters. Let’s talk about how to better harness thread pools.

Why use thread pools?

To understand a technology, you first need to understand why it happened.

If you think about it, if you just want to run a bunch of tasks with multiple threads, you can do it without a Thread pool, just call the start() method and start. So why do we need thread pools?

Thread pools have three main functions:

  • Unified management
  • Reuse threads
  • Control the number of concurrent requests

Unified management is easy to understand, thread pool is actually a thread scheduling system. There is a scheduler thread in the thread pool, which is used to manage various tasks and transactions in the whole thread pool, such as thread creation, thread destruction, task queue management, thread queue management, and so on.

Reusing threads is the biggest advantage of thread pools. Because creating and destroying threads is expensive, it is not cost-effective to create a new thread for each task. Thread pooling enables the reuse of threads, allowing one thread to perform multiple tasks, which can greatly save machine resources in scenarios where a large number of threads are required (such as HTTP requests).

Controlling concurrency refers to the use of thread pools to control the number of threads running at the same time. We know that the advantage of multithreading lies in the use of the computer’s multi-core processing ability, but the number of computer cores is limited, such as 4 cores, 8 cores, etc., if the number of threads is too much, switching threads have the overhead of context, but will let the throughput of the whole machine decline.

Throughput refers to the number of tasks that can be processed per unit of time.

Principle of thread pool

Now that you know why thread pools are used, let’s look at how they work.

Let’s start with the previous figure:

Thread pool schematic

Then let’s explain some of the concepts in the picture below:

“Core threads” : There are two types of threads in the thread pool, core and non-core threads. Core threads are created by default and persist in the thread pool, even if the core thread does nothing (permanent job), while non-core threads are destroyed if they are idle for long periods of time (temporary worker).

“Task queue” : Wait queue, which maintains Runnable task objects waiting to be executed. It is a thread-safe blocking queue.

“Thread pool full” : It means that the total number of core threads + non-core threads reaches the threshold set by the thread pool.

“Reject policy” : When the thread pool is full, it means that the current thread pool is unable to handle more tasks. What if new tasks come in? So when creating the thread pool, you can specify this rejection policy.

Seven arguments to the thread pool

The thread pool principle is described above, and the various thresholds mentioned in it can be specified in the thread pool constructor. Java uses the ThreadPoolExecutor class to implement a thread pool. It has several overloaded constructors that take anywhere from five to seven arguments, but ultimately all call the same constructor with seven arguments, which we’ll look at separately.

// A seven-argument constructor
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
 long keepAliveTime,  TimeUnit unit,  BlockingQueue<Runnable> workQueue,  ThreadFactory threadFactory,  RejectedExecutionHandler handler) Copy the code

The maximum number of core threads, the maximum number of threads (core + non-core), the non-core idle time, and the unit of non-core idle time.

Then introduce the next three parameters.

workQueue

The task queue, which is a thread-safe BlockingQueue BlockingQueue

, is an interface that has many implementations. Task queues are also key to how thread pools control concurrency. There are several common blocking queue implementations:

  • LinkedBlockingQueue: A chained blocking queue. The underlying data structure is a linked list. The default size isInteger.MAX_VALUE, you can also specify the size;
  • ArrayBlockingQueue: an ArrayBlockingQueue. The underlying data structure is an array.
  • SynchronousQueue: SynchronousQueue with 0 internal capacity. Each PUT operation must wait for a take operation and vice versa.
  • DelayQueue: DelayQueue. The element in the queue can only be fetched from the queue when the specified delay time expires.

In general, LinkedBlockingQueue and ArrayBlockingQueue are more common. Which one you choose depends on whether you want to limit the number of task queues.

threadFactory

Create thread factory, used to batch create threads, set some parameters when creating threads, such as thread name, daemon thread, thread priority, etc. ThreadFactory is also an interface. If not specified, a DefaultThreadFactory is created using DefaultThreadFactory.

Many times we will implement a ThreadFactory, specifying the name prefix of the thread, so that when we check the problem, we can immediately see that the thread was created in the thread pool.

handler

Reject processing policy: If the number of threads is greater than the maximum number of threads, the reject processing policy is adopted. The four reject processing policies are as follows:

  • “ThreadPoolExecutor. AbortPolicy” : “default refuse handling strategy”, discarding the task and throw RejectedExecutionException anomalies.
  • “ThreadPoolExecutor. DiscardPolicy” : discard the new task, but does not throw an exception
  • “ThreadPoolExecutor. DiscardOldestPolicy” : discard queue head (the oldest) of tasks, and trying to execute a program again (if fail again, repeat this process).
  • “ThreadPoolExecutor. CallerRunsPolicy:” by the calling thread to handle the task.

How do you reuse threads?

We mentioned three benefits of thread pooling: unified management, reuse of threads, and control of the number of concurrent threads. Unified management is embodied in threadFactory, and controlling the number of concurrent threads is embodied in workQueue. So how does a thread pool reuse threads?

ThreadPoolExecutor creates a thread by encapsulating it as a “worker worker” and placing it in a “worker worker group.” The worker then repeatedly takes tasks from the blocking queue to execute them. The Worker is an inner class that inherits AQS and implements Runnable:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    / / to omit
}
Copy the code

Note that the “worker thread group” is not the aforementioned workQueue, but rather a HashSet:

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

After the worker is created, it will constantly take new tasks out of the task queue and then call the run() method of this task. The source code is in the worker.runworker method.

So what do you see here? We use the execute(Runnable Command) method of the Thread pool to throw the Thread into the Thread pool. Instead of creating a new Thread and calling the start method to start it, each worker directly calls the run() method to execute the Thread. This completes the purpose of reusing threads.

What should I pay attention to when using thread pools?

Most importantly, pay attention to the parameters. Each parameter needs careful consideration, especially the number of core threads, the maximum number of threads, and the lifetime of non-core threads.

How to configure the parameters of a thread pool is a challenge that requires you to consider many aspects, especially if your program has more than one thread pool. It also depends on how many tasks you have to do, so it’s best to do some research and estimate ahead of time.

The number of core threads should not be too large, usually twice the number of CPU cores.

After understanding the working principle, you can set the parameters of the thread pool based on the service scenario.

Most of the time it is the core thread that does the work, and the non-core thread is only started when the task queue is full. If you use a block queue based on a linked list, its maximum length is integer.max_value, and a large number of tasks may result in an OOM.

Therefore, when the number of tasks can be roughly estimated, especially when you are executing something like a self-written task, it is recommended to use an array-based blocking queue to limit the length of the blocking queue. If the length is exceeded, temporary threads can be started to handle it, increasing system throughput.

The rejection strategy is also important. If the task is not important, it can be discarded. If the task is so important that it affects the main logic of the application, it is better to throw the exception.

The JDK provides a thread pool builder class Executors that provides static methods for easily creating special thread pools. It is also the constructor of the ThreadPoolExecutor of the call, but wrapped up to look more semantic.

In fact, if you understand the principle of thread pool, you can take a look at the source code of these several static methods, see what parameters they are used respectively, for their own future configuration of thread pool parameters also have some reference value.

If you want to learn more about Java threads, go to Github and search For RedSpider1/ Concurrent, which is a comprehensive open source book about Java multithreading and covers the majority of Java multithreading. Welcome to star, issue, pr.

About the author

Wechat public number: made up a process

Personal website: https://yasinshaw.com

Pseudonym Yasin, a programmer with depth, attitude and warmth. After work to share programming technology and life, if you like my article, you can easily “follow” the public account, also welcome to “forward” to share with your friends ~

Reply “interview” or “study” on the official account to receive corresponding resources oh ~

The public,