Welcome to The 24th article in this series, and the first in brick and Stone.
In the Diamond series, we’ll learn about the framework and utility classes related to thread pools. As the first article in the platinum series, we’ll go into depth on the application and principles of thread pools.
Thread pools are one of the top priorities in concurrent programming, both in actual project development and in interviews. Therefore, a mastery of thread pools is a must for every Java developer.
In this article, we will take a look at the application scenarios and design principles of thread pools. After understanding the internal structure of thread pools, we will take an in-depth look at thread pools in Java. The full text is about 25,000 words, the length is long, when reading, it is recommended to look at the table of contents first.
Why use thread pools
In the previous series of articles, you’ve learned that multithreading can speed up task processing and improve system throughput. So, does this allow us to create new threads frequently? The answer is no. Not only is it expensive to create and enable new threads frequently, but an infinite number of threads is bound to cause a sharp increase in administrative costs. Thus, to balance the benefits and costs of multithreading, thread pools were born.
1. Usage scenarios of thread pools
Producer and consumer issues are a typical application scenario for thread pools. When you have a steady stream of tasks to work on, you need to create multiple threads in order to speed up the process. So, how do you manage these tasks and multithreading? The answer is: thread pools.
The application of the Pooling principle of thread pools is not limited to Java, but is widely used in MySQL and many distributed middleware systems. When we link to a database, we use thread pools to manage the link; When we use Tomcat, we also use thread pools to manage request links. So, when you have a batch of tasks that require multiple threads, then you basically need to use thread pools.
2. Benefits of thread pools
The benefits of thread pools are mainly reflected in three aspects: system resources, task processing speed, and related complexity management, which are mainly shown in:
- Reduce the system resource overhead: By reuse the worker threads in the thread pool, avoid creating new threads frequently, can effectively reduce the system resource overhead;
- Improve the task execution speed: When a new task arrives, it is directly transferred to an existing thread without creating a new thread, which can effectively improve the task execution speed.
- Efficient management of tasks and worker threads: Mechanisms for task management and worker thread management are provided within the thread pool.
Why is it expensive to create threads
By now you know that creating new threads frequently has an additional cost, so we use thread pools. So what is the cost of creating a new thread? You can refer to the following points:
- When a thread is created, the JVM must allocate and initialize a large chunk of memory for the thread stack. Call stack frames for each thread method are stored here, including local variables, return values, constant pools, etc.
- System calls are made to the host machine when creating and registering native threads;
- The descriptors need to be created, initialized, and added to the JVM’s internal data structures.
Also, in the sense that as long as the thread is alive, it consumes resources, which is not only expensive, but wasteful. For example, thread stacks, reachable objects that access the stack, JVM thread descriptors, operating system native thread descriptors, and so on, are all continuously occupied as long as the thread is alive.
While the cost of creating threads may vary between different Java platforms, in general, none is cheap.
3. Core components of the thread pool
A complete thread pool should contain the following core parts:
- Task submission: Provides an interface to receive task submission;
- Task management: Select the appropriate queue to manage the submitted tasks, including the setting of the rejection policy;
- Task execution: The submitted task is executed by the worker thread;
- Thread pool management: includes basic parameter setting, task monitoring, and worker thread management.
How to manually create a thread pool
You now know what a thread pool does and what its core components are. To better understand the composition of a thread pool, in this section we will manually create a simple thread pool in four simple steps. Of course, the sparrow is small and has all the organs. If you can make your own thread pools manually, it will be a breeze to understand subsequent thread pools in Java.
1. Thread pool design and production
Step 1: Define a kingthreadpool: TheKingThreadPool, which is the real protagonist of this handcraft. This thread pool contains task queue management, worker thread management, and provides construction parameters that can specify queue types, as well as a task submission entry and a thread pool shutdown interface. You see, while it may seem like a very small one, the core components of a thread pool are already there, and you can extend it to a more mature thread pool on top of that.
/** * King thread pool */
public class TheKingThreadPool {
private final BlockingQueue<Task> taskQueue;
private final List<Worker> workers = new ArrayList<>();
private ThreadPoolStatus status;
/** * Initialize build thread pool **@paramWorksNumber Number of worker threads in the thread pool *@paramTaskQueue taskQueue */
public TheKingThreadPool(int worksNumber, BlockingQueue<Task> taskQueue) {
this.taskQueue = taskQueue;
status = ThreadPoolStatus.RUNNING;
for (int i = 0; i < worksNumber; i++) {
workers.add(new Worker("Worker" + i, taskQueue));
}
for (Worker worker : workers) {
Thread workThread = newThread(worker); workThread.setName(worker.getName()); workThread.start(); }}/** * Submit task **@paramTask Indicates the task to be executed */
public synchronized void execute(Task task) {
if (!this.status.isRunning()) {
throw new IllegalStateException("Thread pool not running, stop accepting orders ~");
}
this.taskQueue.offer(task);
}
/** * Wait for all tasks to finish */
public synchronized void waitUntilAllTasksFinished(a) {
while (this.taskQueue.size() > 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) { e.printStackTrace(); }}}/** * Close the thread pool */
public synchronized void shutdown(a) {
this.status = ThreadPoolStatus.SHUTDOWN;
}
/** * Stop thread pool */
public synchronized void stop(a) {
this.status = ThreadPoolStatus.SHUTDOWN;
for(Worker worker : workers) { worker.doStop(); }}}Copy the code
Step 2: Design and build worker threads. The Worker thread is the working thread that will handle the tasks submitted to the thread pool. We call it the Worker. In fact, the Worker definition here is similar to the Worker in the Java Thread pool. It inherits the Runnable interface and encapsulates the Thread. When the Worker is constructed, you can set its name and pass it to the task queue. When the Worker starts, it gets the task from the task queue and executes it. In addition, it provides a Stop method to respond to state changes in the thread pool.
/** * Threads in the thread pool used to execute tasks */
public class Worker implements Runnable {
private final String name;
private Thread thread = null;
private final BlockingQueue<Task> taskQueue;
private boolean isStopped = false;
private AtomicInteger counter = new AtomicInteger();
public Worker(String name, BlockingQueue<Task> queue) {
this.name = name;
taskQueue = queue;
}
public void run(a) {
this.thread = Thread.currentThread();
while(! isStopped()) {try {
Task task = taskQueue.poll(5L, TimeUnit.SECONDS);
if(task ! =null) {
note(this.thread.getName(), ": Obtain a new mission ->", task.getTaskDesc()); task.run(); counter.getAndIncrement(); }}catch (Exception ignored) {
}
}
note(this.thread.getName(), ": Work completed, number of tasks performed:" + counter.get());
}
public synchronized void doStop(a) {
isStopped = true;
if(thread ! =null) {
this.thread.interrupt(); }}public synchronized boolean isStopped(a) {
return isStopped;
}
public String getName(a) {
returnname; }}Copy the code
Step 3: Design and create the task. Tasks are executable objects, so we simply inherit the Runnable interface. It is possible to use the Runnable interface directly, but to make the example more clear, we have added a Task description method to the Task.
/**
* 任务
*/
public interface Task extends Runnable {
String getTaskDesc(a);
}
Copy the code
Step 4: Design the state of the thread pool. As a running framework, a thread pool must have a series of states, such as running, stopped, closed, and so on.
public enum ThreadPoolStatus {
RUNNING(),
SHUTDOWN(),
STOP(),
TIDYING(),
TERMINATED();
ThreadPoolStatus() {
}
public boolean isRunning(a) {
return ThreadPoolStatus.RUNNING.equals(this); }}Copy the code
After the above four steps, a simple thread pool is created. Isn’t it easier to understand the thread pool source code if you start with the above points? The core composition of a thread pool in Java is the same, but more comprehensive and rich in details and so on.
2. Run the thread pool
Now our king thread pool is ready. Next, let’s run it through a scene to see how it works.
Experiment scene: in the canyon forest, kai, Lanling king and Dian Wei are responsible for playing wild, and Angela, Diao Chan and Big Qiao are responsible for the wild monster hunting, a happy canyon barbecue festival is under way.
In this scene, Kai and The King of Lanling are responsible for submitting tasks, while Diao Chan and Big Qiao are responsible for handling them.
In the implementation code below, we define a thread pool with TheKingThreadPool, wildMonsters representing tasks to be submitted, and assign three worker threads to perform the tasks. At the end of the sample code, when all tasks have completed, close the thread pool.
public static void main(String[] args) {
TheKingThreadPool theKingThreadPool = new TheKingThreadPool(3.new ArrayBlockingQueue<>(10));
String[] wildMonsters = {"Brown bear"."Pheasant"."The Wolf"."Rabbit"."Fox"."The deer"."The little Leopard"."Boar"};
for (String wildMonsterName : wildMonsters) {
theKingThreadPool.execute(new Task() {
public String getTaskDesc(a) {
return wildMonsterName;
}
public void run(a) {
System.out.println(Thread.currentThread().getName() + ":" + wildMonsterName + "It's done."); }}); } theKingThreadPool.waitUntilAllTasksFinished(); theKingThreadPool.stop(); }Copy the code
The king thread pool runs as follows:
Worker0: Get a new task -> Gray Wolf Worker1: Get a new task -> pheasant Worker1: Pheasant baked Worker2: Get a new task -> Brown bear Worker2: Brown bear baked Worker1: Get a new task -> Hare Worker1: Hare baked Worker0: Gray Wolf baked Worker1: Get new task -> fawn Worker1: fawn baked Worker2: Get new task -> fox Worker2: Fox baked Worker1: Get new task -> boar Worker1: wild boar has baked Worker0: get to the new task - > baby leopard Worker0: baby leopard has baked Worker0: work has ended, number of mission:2Worker2: Finished work, number of tasks performed:2Worker1: Finished work, number of tasks performed:4
Process finished with exit code 0
Copy the code
As you can see from the results, the effect is exactly as expected. All tasks have been submitted and executed correctly. Worker1 executes 4 tasks, while Worker0 and Worker2 both execute 2 tasks. This is also a normal phenomenon in thread pools.
Have a thorough understanding of Java thread pools
It is relatively easy to understand thread pools in Java after you have manually created thread pools. Of course, the implementation of a thread pool (ThreadPoolExecutor) in Java is much more complicated than the king thread pool. Therefore, it is important to understand the thread pool in a certain structure and context, and to grasp the core points of the thread pool. If you grasp the level of the thread pool, you will not be able to effectively understand its design connotation, and you will not be able to grasp it correctly.
In general, the design of thread pools in Java is centered around “tasks,” which can be summarized by a framework, two cores, and three processes. By understanding these three important concepts, you should be able to understand thread pools at a relatively abstract level.
- A framework: the overall design of the thread pool has a framework, rather than a haphazard composition. So, when learning about thread pools, the first thing to do is to be aware of the framework in three dimensions and not get bogged down in messy details.
- Two Cores: Within the framework of the thread pool, there are two cores around the execution of tasks: the management of tasks and the execution of tasks, which correspond to task queues and worker threads used to execute tasks. Task queue and worker thread are the key parts of the framework to run effectively.
- Three Processes: As mentioned earlier, the overall design of the thread pool is around tasks, so the framework can be divided into three processes: task submission, task management, and task execution.
Analogically, you can think of a frame as a production shop. In this workshop, there is an assembly line, and task queues and worker threads are the two key components of this assembly line. In the process of assembly line operation, it involves different processes such as task submission, task management and task execution.
The following image will help you get a sense of the overall design of the thread pool in three dimensions. The workflow and core components of the thread pool framework are clearly shown in this diagram, which will be the focus of subsequent articles.
1. Overview of thread pool framework design
At the source level, understanding thread pools in Java starts with the following four brothers of concepts and relationships that must be understood.
- ExecutorThe Executor interface is implemented by design as the top-level interface of the thread poolTask submittedwithTask executionBetween,That’s what it’s there for. In Executor, only one method is defined
void execute(Runnable command)
Which is used to perform the commitCan be runThe task. Notice, if you look at it, the parameter to this method is simply calledcommand
“, which is”The command“To show that the object being submitted is not a static object butCan be runThe command. It is also uncertain which thread will execute the command at some point in the future; - ExecutorService: inherits the Executor interface and provides the option on top of thatManagement servicesandExecution result (Futrue)Ability. ExecutorService
submit
Method can return the execution result of a task, andshutdown
Method can be used to shut down the service. By contrast,Executor has a single execution capability, while ExecutorService not only has execution capability, but also provides a simple service management capability; - AbstractExecutorService: as a simple implementation of ExecutorService, this class is implemented with RunnableFuture and newTaskFor
submit
,invokeAny
andinvokeAll
Methods; - ThreadPoolExecutor: This class is the ultimate implementation class for thread pools. It implements the capabilities defined in Executor and ExecutorService and complements the implementation in AbstractExecutorService. In ThreadPoolExecutor, task management policies and thread pool management capabilities are defined, the implementation details of which will be the core of what we will discuss later.
If you still don’t feel comfortable with the differences between the four brothers, you can zoom in and check out the hd image below. As you look at them, pay special attention to their different methods, which means their abilities are different.
As for the overall thread pool execution, the following graph is recommended for your bookkeeping. Although concise, this picture shows the complete process from task submission to task execution. This implementation process is often in the interview of the high-frequency interview questions, be sure to master.
(1) Core attributes of the thread pool
Some of the core attributes of the thread pool are selected as follows, with specific descriptions for individual attributes.
// The thread pool controls the main variables associated with the thread pool
// This variable is very interesting. It will be described in the following paragraphs
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Queue of tasks to be processed
private final BlockingQueue < Runnable > workQueue;
// Set of worker threads
private final HashSet < Worker > workers = new HashSet < Worker > ();
// Create the thread factory used by the thread
private volatile ThreadFactory threadFactory;
// Reject policy
private volatile RejectedExecutionHandler handler;
// Number of core threads
private volatile int corePoolSize;
// Maximum number of threads
private volatile int maximumPoolSize;
// The duration of the idle thread
private volatile long keepAliveTime;
// The main control lock for thread pool changes is used when the number of worker threads is changed, the thread pool state is changed, etc
private final ReentrantLock mainLock = new ReentrantLock();
Copy the code
Special note on CTL fields
Among the core fields of a ThreadPoolExecutor, the other fields may be easy to understand, but CTL needs to be singled out for interpretation.
As the name implies, the CTL field is used for thread pool control. It’s an interesting design, but using a single field can mean two things: the field is actually a combination of two fields:
- RunState: The running state (up to 3 bits) of the thread pool;
- WorkerCount: Number of worker threads (lower than 29 bits).
The values of the two fields are independent of each other. So why this design? This is because the two fields almost always go hand in hand in a thread pool, and if they are not represented by a single field, the locking mechanism is needed to control the consistency of the two fields. Have to say, this field design is more clever.
In the thread pool, there are also ways to easily get the state of the thread pool and the number of worker threads, both of which are obtained by bitwise operations on the CTL.
/** Calculate the current thread pool state */
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
/** Count the number of working threads */
private static int workerCountOf(int c) {
return c & CAPACITY;
}
/** Initialize the CTL variable */
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
Copy the code
There’s a little bit of a caveat about bit operations, if you’re a little confused about bit operations, and if you’re familiar with it you can skip it.
Let’s say A is 15, 1111 in binary; B is equal to 6. It’s 110 in binary.
The operator The name of the describe The sample & Bitwise and If the corresponding bits are all 1, the result is 1, otherwise it is 0 A&B, you get 6, which is 110 ~ According to a non The bitwise inverse operator reverses each bit of the operand, so that 0 becomes 1 and 1 becomes 0. (to A) – 16, 11111111111111111111111111110000 | Bitwise or If the corresponding bits are 0, the result is 0, otherwise it is 1 (A | B) had 15, 1111
(2) The core constructor for the thread pool
ThreadPoolExecutor has four constructors, one of which is the core constructor. You can use these constructors on demand and on demand.
- One of the core constructors: a relatively common constructor that allows you to specify the number of core threads, the maximum number of threads, the duration of thread keepalive, and the task queue type.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue < Runnable > workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
Copy the code
- Core constructor # 2: Instead of the first constructor, you can specify a ThreadFactory in this constructor. Using a ThreadFactory, you can specify thread names, groups, and other personalized information.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue < Runnable > workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
Copy the code
- Core Constructor # 3: The point of this constructor is that you can specify a rejection policy. The task queue rejection policy is described in detail in the following sections.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue < Runnable > workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
Copy the code
- Core Constructor 4: This constructor is the core constructor of ThreadPoolExecutor and provides a comprehensive set of parameters on which all three of the above constructors are based.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue < Runnable > workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
Copy the code
(3) Core methods in the thread pool
/** * Submit a Runnable task and execute it, but return no result */
public void execute(Runnable command){... }/** * submit a Runnable task and return the result */
publicFuture<? > submit(Runnable task){... }/** * Submit a Runnable task and return the result. You can specify default results */
public <T> Future<T> submit(Runnable task, T result){... }/** * Submit a Callable task and execute */
public <T> Future<T> submit(Callable<T> task) {... }/** * Close the thread pool and continue to execute outstanding tasks in the queue, but no new tasks will be accepted */
public void shutdown(a) {... }/** * Immediately close the thread pool, and discard the pending tasks and no more new tasks */
public List<Runnable> shutdownNow(a){... }Copy the code
(4) Thread pool state and life cycle management
As mentioned earlier, the thread pool is like a production workshop, and from the production workshop’s perspective, the production workshop has different states, such as running, down, etc., so the thread pool also has certain states and lifetime.
- Running: In the Running state, tasks can be added to the thread pool and tasks in the queue can be processed normally.
- Shutdown: the thread pool is being Shutdown. In this state, the thread pool does not stop immediately but cannot continue to add tasks to the thread pool until the task execution is complete.
- Stop: Stops. In this state, new tasks will not be received, tasks in the queue will not be processed, and working threads will be interrupted.
- Tidying: a relatively brief intermediate state in which all tasks have ended and all worker threads no longer exist (workerCount==0) and are running
terminated()
Hook method; - Terminated:
terminated()
The operation is complete.
2. How do I submit a task to a thread pool
There are two common ways to submit a task to a thread pool: one that requires the return of the execution result, and one that does not.
(1) Do not care about the task execution result: execute
After a task is submitted to the thread pool via execute(), it will be executed at some point in the future, either by a thread in the current thread pool or by a newly created thread. Of course, if the thread pool should be closed or the task queue is full, then the task will be passed to RejectedExecutionHandler.
(2) Pay attention to the task execution result: submit
After submitting a task to a thread pool via submit(), the mechanism is similar to execute, except that submit() waits for the task to finish and returns the result.
3. How do I manage the submitted tasks
(1) Task queue selection strategy
- SynchronousQueue: Seamless transmission (Direct handoffs). When a new task arrives, it is processed directly by the thread rather than being queued up in the cache. Therefore, if no threads are available when the task arrives, a new thread will be created. Therefore, in order to avoid job loss, the use of SynchronousQueue will require the creation of an infinite number of threads, which should be carefully evaluated.
- LinkedBlockingQueue: An unbounded queue into which new submitted tasks are cached. With an unbounded queue, only threads in the corePoolSize process tasks in the queue. This is independent of maximumPoolSize, which does not create new threads. Of course, you should be aware that if tasks are processed much faster than they are generated, then the infinite growth of the LinkedBlockingQueue could cause problems such as memory capacity.
- ArrayBlockingQueue: a bounded queue that may trigger the creation of a new worker thread. The maximumPoolSize parameter setting takes effect in a bounded queue. When using bounded queues, pay particular attention to the tradeoff between task queue size and the number of worker threads. If the task queue is large but the number of threads is small, the result will be lower system resource usage (mainly CPU), but also lower system throughput. On the other hand, if the task queue is smaller and the number of worker threads is larger, the result is higher system throughput, but also higher system resource usage. Therefore, when using bounded queues, consider the art of balancing and configure a denial policy accordingly.
(2) How to choose an appropriate rejection strategy
When using a thread pool, the rejection policy is one that must be checked because it can cause task loss.
When the thread pool is closed or the task queue is full and no more worker threads can be created, then the new submitted task will be rejected. On rejection, the rejectedExecution(Runnable R, ThreadPoolExecutor Executor) in the RejectedExecutionHandler is called to perform the specific rejection action.
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
Copy the code
Take the execute method as an example. When the thread pool status is abnormal or no new worker threads can be added, the task rejection policy is executed.
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
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);
}
Copy the code
The default rejection policy of a ThreadPoolExecutor is AbortPolicy, which is determined in the property definition. In most scenarios, it is not appropriate to simply refuse a task.
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
Copy the code
- AbortPolicy: the default policy, direct selling RejectedExecutionException exception;
- CallerRunsPolicy: Let the current thread execute by itself. This strategy provides a simple feedback control mechanism that slows down the submission of new tasks;
- DiscardPolicy: Directly discards the task without throwing an exception.
- DiscardOldestPolicy: If the thread pool is not closed at this time, the first task is taken from the head of the queue, discarded, and attempted again. If the execution fails, the process is repeated.
If none of the above four policies are met, you can also customize the RejectedExecutionHandler interface. In fact, in order to balance task loss and system load, it is recommended that you implement your own rejection policy.
(3) Queue maintenance
Thread pools also provide methods for task queue maintenance.
- Gets the current task queue
public BlockingQueue<Runnable> getQueue(a) {
return workQueue;
}
Copy the code
- Removes a task from the queue
public boolean remove(Runnable task) {
boolean removed = workQueue.remove(task);
tryTerminate(); // In case SHUTDOWN and now empty
return removed;
}
Copy the code
4. How do I manage worker threads that execute tasks
(1) Core worker thread
The core thread (corePoolSize) is the minimum number of worker threads that do not allow timeout reclamation. Of course, if you set allowCoreThreadTimeOut, core threads will also time out, which may result in zero core threads. The number of core threads can be specified by the thread pool’s construction parameter.
(2) Maximum worker thread
The maximum number of worker threads is the maximum number of worker threads that can be created by a thread pool to handle existing tasks.
The maximum worker thread can be set through the maximumPoolSize variable in the constructor. Of course, if you are using an unbounded task queue, then this parameter will be null.
(3) How to create a new worker thread
In a thread pool, the creation of new threads is done via a ThreadFactory. You can use the thread pool constructor specify particular ThreadFactory, such as not specified will use the default Executors. DefaultThreadFactory (), the factory have the same ThreadGroup as the thread and created priority (NORM_PRIORITY), Neither is a non-daemon thread.
By setting a ThreadFactory, you can customize the thread name, thread group, daemon state, and so on.
In the Java thread pool ThreadPoolExecutor, the addWorker method is responsible for the creation of new threads.
private boolean addWorker(Runnable firstTask, boolean core) {... }Copy the code
(4) Survival time
Live time refers to the time that a non-core thread can live while idle.
If the number of threads in the thread pool exceeds the value set in corePoolSize, the idle thread will be reclaimed and terminated after the idle time of the idle thread exceeds the time set in keepAliveTime. After the thread is reclaimed, new threads will continue to be created if needed.
Note that keepAliveTime is valid only for non-core threads. If you want to set the keepAliveTime for core threads, you need to use the allowCoreThreadTimeOut parameter.
(5) Hook methods
- Set the action before task execution: beforeExecute
If you want the submitted task to perform certain actions before execution, such as writing to a log or setting a ThreadLocal, etc. Well, you can do this by rewriting beforeExecute.
protected void beforeExecute(Thread t, Runnable r) {}Copy the code
- Set the action after task execution: beforeExecute
If you want the submitted task to perform a specific action after execution, such as writing to a log or catching an exception. Well, you can do this by overwriting afterExecute.
protected void afterExecute(Runnable r, Throwable t) {}Copy the code
- Set the thread pool termination action: terminated
protected void terminated(a) {}Copy the code
(6) Thread pool warm-up
By default, relevant threads are not created immediately after the number of core threads is set, but are created after the task arrives.
If you need to start the core threads beforehand, you can do this by calling prestartCoreThread or prestartAllCoreThreads, and you can verify this by using the ensurePrestart method.
(7) Thread recycling mechanism
When the number of worker threads in the thread pool is greater than the number set by corePoolSize, and there are idle threads that have been idle for longer than the keepAliveTime setting, the idle threads will be reclaimed to reduce unnecessary resource waste.
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while(task ! =null|| (task = getTask()) ! =null) {... }finally {
processWorkerExit(w, completedAbruptly); // Take initiative to recycle yourself}}Copy the code
(8) Adjusting the number of threads
The reasonable setting of worker threads in the thread pool is related to the balance between system load and task processing speed. To be clear, there is no one-size-fits-all formula for how to set up core threads. Each business scenario is unique in its own way, and cpu-intensive and IO-intensive tasks differ greatly. Therefore, when using thread pools, it is a case-by-case approach, but you can run the results continuously to optimize the pool.
5. Example of thread pool usage
Let’s use the ThreadPoolExecutor implementation as an example of the use of thread pools, using the same scenario as the manual thread pool part. From the code, the use of ThreadPoolExecutor is similar to the use of TheKingThreadPool.
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3.20.1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue < > (10));
String[] wildMonsters = {"Brown bear"."Pheasant"."The Wolf"."Rabbit"."Fox"."The deer"."The little Leopard"."Boar"};
for (String wildMonsterName: wildMonsters) {
threadPoolExecutor.execute(new RunnableTask() {
public String getTaskDesc(a) {
return wildMonsterName;
}
public void run(a) {
System.out.println(Thread.currentThread().getName() + ":" + wildMonsterName + "It's done."); }}); } threadPoolExecutor.shutdown(); }Copy the code
6. Executors class
Executors are a JUC ThreadPoolExecutor and ThreadFactory design such as a utility class. Using Executors, you can easily create different types of thread pools. Internally, of course, it’s mostly done by passing specific parameters to the construct of a ThreadPoolExecutor, so there’s no mystery. Some commonly used tools are as follows:
- Create a thread pool with a fixed number of threads
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Copy the code
- Create a thread pool with only one thread
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1.1.0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
Copy the code
- Create a cache thread pool: This pool does not set the number of core threads and dynamically creates threads based on the task’s data. When the task is finished, the threads are gradually reclaimed, meaning that all threads are temporary.
public static ExecutorService newCachedThreadPool(a) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Copy the code
7. Thread pool monitoring
As a running framework, ThreadPoolExecutor is both simple and complex. Therefore, its internal monitoring and management is very necessary. ThreadPoolExecutor also provides methods by which you can obtain important state and data about a thread pool.
- Gets the thread pool size
public int getPoolSize(a) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Remove rare and surprising possibility of
// isTerminated() && getPoolSize() > 0
return runStateAtLeast(ctl.get(), TIDYING) ? 0 :
workers.size();
} finally{ mainLock.unlock(); }}Copy the code
- Gets the number of active worker threads
public int getActiveCount(a) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int n = 0;
for (Worker w: workers)
if (w.isLocked())
++n;
return n;
} finally{ mainLock.unlock(); }}Copy the code
- Gets the maximum thread pool
public int getLargestPoolSize(a) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
return largestPoolSize;
} finally{ mainLock.unlock(); }}Copy the code
- Gets the total number of tasks in the thread pool
public long getTaskCount(a) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
long n = completedTaskCount;
for (Worker w: workers) {
n += w.completedTasks;
if (w.isLocked())
++n;
}
return n + workQueue.size();
} finally{ mainLock.unlock(); }}Copy the code
- Gets the total number of completed tasks in the thread pool
public long getCompletedTaskCount(a) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
long n = completedTaskCount;
for (Worker w: workers)
n += w.completedTasks;
return n;
} finally{ mainLock.unlock(); }}Copy the code
How to develop a good habit of using thread pools correctly
1. Thread pool usage risk
While there are many benefits to using thread pools, there is no such thing as a free lunch. In addition to the benefits of thread pools, there are a few caveats to avoid potholes:
- Too large or too small a thread pool setting is not appropriate. If there are too many threads in the thread pool, local processing speed will increase, but the overall performance of the application will be affected. If the pool has too few threads, it may not provide the desired performance boost.
- Like any other multithread, deadlocks can occur in a thread pool. For example, one task waits for another task to finish, but there is no thread to execute the waiting task, which is why dependencies between tasks are avoided.
- It takes too long to add a task to the queue. Procedure If the task queue is full, external threads are blocked from adding tasks to the queue. So, to prevent external threads from blocking for too long, you can set the maximum wait time;
To mitigate these risks, you should be careful when setting the type and parameters of the thread pool. It’s a good idea to do a stress test before the launch.
2. The recommended position for creating a thread pool
Although by Executors create a thread is more convenient, but the Executors of encapsulation details blocked some important parameters, and these parameters is very important to the thread pool, therefore in order to avoid because do not understand Executors and incorrect use of thread pool, It is recommended that you create a ThreadPoolExecutor directly through the constructor parameter of the ThreadPoolExecutor.
3. Avoid using unbounded queues
Seriously, you should avoid using unbounded queues to manage tasks at all times. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
summary
That’s all about Java thread pools. In this article, we explored the application scenarios, core components, and principles of thread pools, created a thread pool by hand, and further explored the Implementation of a Java thread pool, ThreadPoolExecutor. Although the overall size of this article is large, the scope of thread pools is too extensive to cover in a single article, and some important topics, such as how to handle exceptions in a thread pool and how to gracefully close a thread pool, are not covered.
Threading pools are not easy to master, so follow the advice at the beginning of this article by first understanding the problem to be solved, then understanding the core principles of their composition, and finally delve into the source code in Java. This way, looking at the source code with known concepts makes it easier to understand the design of the source code.
At the end of the text, congratulations on your another star ✨
The teacher’s trial
- Consider: how to ensure that the thread pool does not lose tasks.
Further reading and references
- “King concurrent course” outline and update progress overview
- Stackoverflow.com/questions/5…
- Tutorials.jenkov.com/java-concur…
About the author
Pay attention to [technology 8:30], get the article updates in time. Pass on quality technical articles, record the coming-of-age stories of ordinary people, and occasionally talk about life and ideals. 8:30 in the morning push author quality original, 20:30 in the evening push industry depth good article.
If this article is helpful to you, welcome to like, follow, supervise, we together from bronze to king.