Java Concurrency: a summary of the basic principles of thread pool

1, Why choose a thread pool and create threads when not in use?

Pooling Technology: prepare some resources in advance, and reuse these prepared resources when needed. Common pooling technologies include thread pool, memory pool, database connection pool and HttpClient connection pool.

As a practice of pooling technology, thread pool is essentially the same idea. It prepares resources in advance for emergencies. Therefore, compared with task re creation, thread pool has the following advantages:

  • Reduce resource consumption: reduce the consumption caused by thread creation and destruction by reusing the created threads.
  • Improve response speed: when a task arrives, it can be executed immediately without waiting for the thread to be created.
  • Improve the manageability of threads: threads are scarce resources. If they are created without restrictions, they will not only consume system resources, but also reduce the stability of the system.

2, What happens when a task enters the thread pool?


As shown in the figure above, when a new task enters the thread pool, the processing flow is as follows:

Judge whether all threads in the core thread pool are executing tasks. If not, create a worker thread to execute this task
When the core thread pool is full, it enters the work queue and waits
When the work queue is full, judge whether the thread pool reaches the maximum number of threads. If not, create a worker thread to execute this task
If both the work queue and the thread pool are full, it is left to the saturation strategy to handle the task

3, How to use a thread pool?

Creation of thread pool

In the process of creating a thread pool, we use the underlying new ThreadPollExecutor,

package com.markor.template.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * @describe:
 * @author: caichangmeng <modules@163.com>
 * @since: 2018/10/22
 */
@Configuration
public class ThreadConfig {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setMaxPoolSize(7);
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        return taskExecutor;
    }
}


Its source code is as follows

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) //Check whether the input parameters are abnormal
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException(); //Check whether the work queue, thread factory and reject policy are null pointers
              //Start assigning values to attributes
        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;
    }

You can see the seven parameters of thread pool creation:

  • corePoolSize: the base size of the thread pool
  • Runnabletasqueue: task queue
  • ThreadFactory: used to set the factory for creating threads. You can set meaningful names for threads through the thread factory
  • RejectedExecutionHandler: saturation policy, AbortPolicy by default, jdk1 5 provides the following four strategies
  1. AbortPolicy: throw an exception directly
  2. CallerRunsPolicy: only the thread of the caller runs the task
  3. DiscardOldestPolicy: discards the latest task in the queue and executes the current task
  • DiscardPolicy: do not process discards
  • keepAliveTime: the time that the worker thread of the thread pool remains alive after it is idle
  • TimeUnit: the unit of thread activity holding time

Submit task to thread pool

At present, it is divided into executre() and submit() methods

//execute() submits a task that does not require a return value
threadPool.execute(new Runnable() {
            @Override
            public void run() {
                //TODO 
            }
        });
//submit() submits a task that requires a return value
Callable myCallable = new Callable() {
  @Override
  public String call() throws Exception {
    Thread.sleep(3000);
    return "call Method return value";
  }
};
Future future = threadPool.submit(myCallable);
String futureRes = future.get();

Close thread pool

//All threads executing tasks are stopped
threadPool.shutdownNow();
//Interrupt only threads that do not execute tasks
threadPool.shudown();

Reasonable configuration of thread pool

1. Configuration of threads:
In the current project of the company, a simple HttpServer is used to expose the contents of prometheus metrics. fixThreadPool is set, and the number of threads is set to 5, which is the empirical value obtained according to Ncpu * 2.

2. It is recommended to use bounded queue
Bounded queue can increase the stability and early warning ability of the system. It can be set larger as needed, such as several thousand.

Keywords: Java Back-end JUC

Added by shauny17 on Thu, 06 Jan 2022 03:33:44 +0200