Thread pool detailed analysis ThreadPoolExecutor

Why is the Thread pool used when using threads,? What are the advantages and differences compared with creating Thread t...
Introduction to thread pool
Advantages of thread pool:
Four ways to create common thread pools
TreadPoolExecutor construction parameters
Thread pool creation and scheduling management
Summary:

Why is the Thread pool used when using threads,? What are the advantages and differences compared with creating Thread threads by users
Before understanding the thread pool, take a look at the following figure to help understand the following contents

Introduction to thread pool

Thread is a very heavy resource. Its creation and destruction are resource consuming operations. Threads in java belong to KLT (kernel level threads), so thread creation is completed by the operating system kernel. When threads need to be created, mapping them to the external interface of the system through the jvm and allocating thread resources through the operating system is a relatively heavy operation, In order to avoid excessive consumption of threads, it is necessary to make rational use of threads. Thread pool is equivalent to a thread cache, which allocates and schedules the requested threads
Thread pool is suitable for some operations with large amount of tasks and short processing time

Advantages of thread pool:

  • Reuse existing threads, reduce thread creation and destruction operations, and improve performance
  • Improve the execution efficiency. When a large number of threads flow in, threads will be allocated and scheduled reasonably
  • Facilitate thread management, scheduling and monitoring

Four ways to create common thread pools

Executors class: mainly used to provide thread pool related operations
newCachedThreadPool,newFixedThreadPool,newSingleThreadExecutor,newScheduleThreadPool.

1. newCachedThreadPool()
A cacheable thread pool with 0 core threads and integer.max non core threads_ Value can be created in theory, but because it is a resource consuming operation, the so-called resource consuming operation means that when thousands of threads are created, your CPU utilization will reach 100%, which means that your computer's CPU is only busy creating threads at a certain time
Simple usage

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); newCachedThreadPool.execute(runnable object);

2. newFixedThreadPool(int nThreads)
A thread pool with a fixed size is created. The number of threads created is nThread, and the number of core threads is ntheadds. There are no non core threads. We will talk about the core and non core threads later. When the received threads exceed ntroads, the threads will be put into its own LinkedBlockingQueue blocking queue. When the threads in the working thread finish executing, they will execute the tasks in the blocking queue, This involves the execution priority of a thread, which will be discussed later

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(20); newFixedThreadPool.execute(runnable object);

3. newSingleThreadExecutor()
Single means single, that is, only one core thread will be created in this thread pool, and the other received tasks will be saved in the LinkedBlockingQueue queue for waiting. When executing a large number of tasks, this will take more time, so it needs to be used according to different scenarios

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); newSingleThreadExecutor.execute(runnble);

4. newScheduledThreadPool()
A thread pool that can execute timed and periodic tasks. The maximum core thread is corePoolSize and the maximum non core thread is integer.max_ Value, before JDK1.5, timers were more used to implement timed and periodic tasks. Timers have defects in managing delayed tasks, because timers only create one thread when executing threads. If task execution takes time, subsequent threads will delay execution. When timers throw exceptions, all subsequent tasks will not be executed. Timers depend on system time when executing periodic tasks, If the current system time changes, there will be some execution changes. The execution of time-consuming tasks by ScheduledThreadPoolExecutor will not affect the execution of other tasks and will not be affected by the change of system time

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("Test test ScheduledThreadPoolExecutor"); } }, 1, 1, TimeUnit.SECONDS);

The four thread pools created look different on the surface. In fact, within their methods, they are still the created ThreaPoolExecute class, including newScheduledThreadPool(). The construction of the ScheduledThreadPoolExecutor class created internally also calls the construction method of the parent class (ThreadPoolExecutor) to create the thread pool
Then, the ThreadPoolExecutor is created in all four ways, and its status in the thread pool can be seen

TreadPoolExecutor construction parameters

Let's take a look at the ThreadPoolExecutor class
Let's learn about its parameters through the construction method of ThreadPoolExecutor

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;

Meaning of each parameter:

  • corePoolSize: the size of the core thread in the thread pool
  • maximumPoolSize: the maximum number of threads in the thread pool. This number is the sum of core threads and non core threads. Many people mistakenly think it is the size of non core threads
  • keepAliveTime: the maximum idle time of non core threads in the thread pool. If it exceeds the time, the idle thread will be destroyed
  • Unit: the time unit of keepalivetime
  • workQueue: the blocking queue for storing runnable tasks, also known as buffer queue. When the core thread exceeds the corePoolSize, the task will be stored in the blocking queue
  • treadFactory: the interface for creating threads. The four creation methods use Executors.defaultThreadFactory(). Its function creates a DefaultThreadFactory object. By default, it implements the newThread(Runnable) method for creating threads in the thread pool
  • handler: reject policy. When the number of threads in the thread pool reaches the maximum poolsize and the blocking queue reaches the maximum, the task reject policy will be executed
    There are usually four rejection strategies:
    1. ThreadPoolExecutor.AbortPolicy: discards the task and throws RejectedExecutionException.
    2. ThreadPoolExecutor.DiscardPolicy: discards the task without throwing an exception.
    3. Threadpoolexecutor.discardolddestpolicy: discard the task at the top of the queue and resubmit the rejected task
    4. ThreadPoolExecutor.CallerRunsPolicy: the calling thread (the thread submitting the task) handles the task, that is, the calling thread defines a rejection policy

Before moving on, let's talk about core threads and non core threads

Core thread: when the task is less than the set number of core threads, a fixed thread will be created. The unique property is that when a task comes in, a thread will be directly created for execution and will not be destroyed. Unless allowCoreThreadTimeOut(true) is called, it will be destroyed after timeout
Non core thread: when the number of core threads has reached the maximum and the queue is saturated, the non core thread is different from the core thread. First, the creation priority is different, and second, the survival time is different. When there is no task to execute, the non core thread will be destroyed after waiting for a fixed time
Both are essentially new Thread(), but they are different because of the characteristics such as creation priority and survival time

Thread pool creation and scheduling management

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(5)); for (int i = 0; i < 14; i++) { MRunnable mRunnable = new MRunnable(i); threadPoolExecutor.execute(mRunnable); } threadPoolExecutor.shutdown();

Start to create the ThreadPoolExecutor object. The number of core threads is 5 and the number of non core threads is 5. The idle timeout is 200ms. The ArrayBlockingQueue queue queue is used. Although the ThreadFactor thread factory and the rejection policy to be executed are not passed in here, but

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);

You can see that another constructor is called by default inside its constructor, a DefaultThreadFactory object is created inside Executors.defaultThreadFactory(), and a ThreadPoolExecutor.AbortPolicy rejection policy is passed in

Looking back, the for loop creates 14 work tasks. After calling the execute() method, it is executed inside the execute() method

if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); //ctl is an AtomicInteger object, and the core is CAS security }

workerCount() returns the size of the current thread pool. When it is less than the number of core threads, addWorker() method will be called

In the thread pool, to ensure the data synchronization of the thread pool under the premise of different thread access, you need a lightweight and efficient security lock. At this time, AtomicInteger is used. In addWorker(), it is realized through CAS synchronization mechanism. The workerCountOf(c) method involves this strategy. I only mention this term here. Readers are learning more about the specific content

OK, enter the addworker () method. After judging the number of thread pool States, you will start to create threads to see what happens in addworker ()

try { w = new Worker(firstTask);//The thread will be created internally using the initialized threadfactory, final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); ....... workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; ....... } finally { mainLock.unlock(); } if (workerAdded) { t.start(); //Thread start workerStarted = true; }

If the preceding conditions are established, you can see from the code that you will create a worker object and add it to the core thread collection, and record the size change of the collection. Finally, call the start() method to start the thread, and callback runWorker() in this method.

Runnable task = w.firstTask; w.firstTask = null; while (task != null || (task = getTask()) != null) { //The current thread will continuously fetch and execute tasks w.lock(); .... task.run(); //The run() of the task is called in runwoker(). }

In runWorker, the loop will be used to continuously execute tasks (firstTask is the first task passed in when creating a thread. After executing a task, the current thread will continue to return to the loop and look for tasks in getTask,

In getTask(), the

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

To determine whether the current thread calling this method is a core thread. If yes, timed is false, otherwise it is a non core thread. Why do you make this judgment?

private Runnable getTask() { ... Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS): workQueue.take();

Before describing, let's talk about the poll() and take() methods. These two methods are similar to remove(). They both delete the header element. Poll and take will return the header element to be deleted to the caller. The difference is that when there are no elements in the queue:
poll(time, unit): it will not directly return null, but wait for a while. The waiting time is time and the unit is unit. After the time expires, it will return null
take(): wait until there are elements in the queue, delete and return
Remove(): when the queue is empty, the remove() method will report NoSuchElementException error

This is because when a thread fetches a task from the task queue, when the queue size==0 and the task in the queue has been fetched, if the current thread is the core thread, take() will be called , block until a task enters the queue and returns to the task. If no task is blocked, the current thread will always exist. In this way, we know that after the core thread is created, it will always exist unless the core thread is forced to terminate
If the current thread is a non core thread and timed = true, workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) will be called Block according to the keepAliveTime parameter. After blocking the keepAliveTime unit time, it is found that there are still no tasks in the queue, and the method will directly return null. At this time, if the loop judgment condition of the runWorker method is not tenable, exit the loop and continue the subsequent finishing work, then the thread will eventually be destroyed

Next, let's look at how the thread pool allocates tasks and schedules threads

if (workerCountOf(c) < corePoolSize) {//If the conditions are met, create the core thread if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) {//Exceeds the core thread and is added to the blocking queue int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) //When the program reaches this point, the thread is still 0. It is likely that when initializing the thread pool, corepoolsize=0 addWorker(null, false); } else if (!addWorker(command, false)) //Create a non core thread reject(command);

In summary, three if judgments are made:

  • The first if (workercountof (c) < corepoolsize): the parameter of the addWorker() method executed is true, and the core thread is created

  • The second if (isrunning (c) & & workqueue. Offer (command)): when the thread pool is running, call workerQueue.offer(). This method is to add tasks to the work queue, similar to add(), in else()
    Another judgment is made in if. The current number of thread pools is equal to 0? Why do we judge whether it is equal to 0? We can think about it. All the previous assumptions have core threads, so when the client creates a thread pool object, the corepoolsize parameter is 0? This explains. Create a non core thread, the parameter is null, and the internal will look for tasks in the queue (call getTask())

  • The third if (!addWorker(command,false)) if the first two are not satisfied, create a non core thread. If it fails, one reason is to throw an exception, the other is that the number of non core threads reaches the maximum, then call reject()

int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) //The current thread status is not Running. Delete the submitted task reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command);

reject() appears twice in execute(). This method executes the reject policy. First, when the current thread is not running, delete the submitted task and execute the reject policy. The second time, the second parameter of addWorker in if is false, which is a non core thread,

private boolean addWorker(Runnable firstTask, boolean core){ //Once false is returned, it indicates that thread creation failed ... wc >= (core ? corePoolSize : maximumPoolSize)) return false;

The formal parameter variable of the second parameter is core. When it is false, the current number of thread pools wc is compared with the maximum poolsize passed in during initialization. When it is greater than, it indicates that the maximum number of threads has been reached or exceeded, and the program can run to else if (!addWorker(command, false)) reject(command); it indicates that the queue has also been saturated, so the judgment condition is valid and the reject policy is executed

OK, I've spent such a long time introducing the execution process of thread pool and making a summary

Summary:

This article mainly understands the thread execution process from the perspective of source code, and briefly introduces some related contents When there are tasks, the thread pool will first create core threads ------ then join the queue ------ create non core threads after the queue is saturated ------ reject the tasks after they are all saturated and execute the rejection policy It should also be noted that when the core thread has no task, it calls the take() method and is blocked all the time, so it needs to call executor.allowCoreThreadTimeOut(true); set to true, it means that the core thread is allowed to be destroyed, so that the core thread will not drag the thread and cannot be released when there is no task This paper describes the methods of operating the blocking queue, such as offer,take(),remove(),poll(), and the concepts of RejectedExecutionHandler, core thread and non core thread

30 October 2021, 19:39 | Views: 9188

Add new comment

For adding a comment, please log in
or create account

0 comments