Java Basic Summary (89)-Explain the meaning of the parameters of ThreadPoolExecutor and the source code execution process in detail?

Original link

table of Contents

1. Typical answer

2. Test site analysis

3. Knowledge expansion

3.1 execute() VS submit()

3.2 Rejection strategy of thread pool

3.3 Customized rejection policy

3.4 ThreadPoolExecutor extension

4. Summary


The thread pool is to avoid the performance consumption caused by the frequent creation and destruction of threads . It is a pooling technology that puts the created threads into the "pool" and reuses the existing ones when tasks come. The thread does not need to wait for the creation process, so that the response speed of the program can be effectively improved. But if you want to talk about thread pools, you must not do without ThreadPoolExecutor. The thread pool is specified in Alibaba's "Java Development Manual":

Thread pools are not allowed to be created using Executors, but through ThreadPoolExecutor . This processing method allows readers to be more clear about the running rules of the thread pool and avoid the risk of resource exhaustion.

Note: The disadvantages of the thread pool object returned by Executors are as follows:

  • FixedThreadPool and SingleThreadPool: The allowed request queue length is Integer.MAX_VALUE, which may accumulate a large number of requests, leading to OOM.
  • CachedThreadPool and ScheduledThreadPool: The allowed number of threads to be created is Integer.MAX_VALUE, which may create a large number of threads, resulting in OOM.

In fact, when we look at the source code of Executors, we will find that the underlying methods of Executors.newFixedThreadPool(), Executors.newSingleThreadExecutor() and Executors.newCachedThreadPool() are all implemented by ThreadPoolExecutor, so we will focus on understanding ThreadPoolExecutor in this lesson. Relevant knowledge, such as what core parameters does it have? How does it work?

1. Typical answer

The core parameters of ThreadPoolExecutor refer to the parameters that need to be passed during construction. The construction method 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,且必须大于 corePoolSize
        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;
}

The first parameter: corePoolSize  represents the number of resident core threads in the thread pool. If set to 0, it means that the thread pool will be destroyed when there is no task; if it is greater than 0, the number of threads in the thread pool will be guaranteed to be equal to this value even when there is no task. But it should be noted that if this value is set relatively small, threads will be created and destroyed frequently (the reasons for creation and destruction will be discussed in the second half of this lesson); if it is set relatively large, system resources will be wasted. So developers need to adjust this value according to their actual business.

The second parameter: maximumPoolSize  represents the maximum number of threads that can be created when the thread pool has the most tasks. Officially, this value must be greater than 0 and must be greater than or equal to corePoolSize. This value is only used when there are many tasks and cannot be stored in the task queue.

The third parameter: keepAliveTime  represents the survival time of the thread. When the thread pool is idle and exceeds this time, the excess threads will be destroyed until the number of threads in the thread pool is destroyed equal to corePoolSize. If the maximumPoolSize is equal to corePoolSize, then the thread The pool will not destroy any threads when it is idle.

The fourth parameter: unit represents the unit of  survival time, which is used in conjunction with the keepAliveTime parameter.

The fifth parameter: workQueue  represents the task queue executed by the thread pool. When all threads in the thread pool are processing tasks, if a new task comes, it will be cached in this task queue and queued for execution.

The sixth parameter: threadFactory  represents the thread creation factory, this parameter is generally used less, we usually do not specify this parameter when creating a thread pool, it will use the default thread creation factory method to create threads, the source code is as follows:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    // Executors.defaultThreadFactory() 为默认的线程创建工厂
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}
// 默认的线程创建工厂,需要实现 ThreadFactory 接口
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }
    // 创建线程
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon()) 
            t.setDaemon(false); // 创建一个非守护线程
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY); // 线程优先级设置为默认值
        return t;
    }
}

We can also customize a thread factory by implementing the ThreadFactory interface, so that we can customize the name of the thread or the priority of thread execution.

The seventh parameter: RejectedExecutionHandler  represents the rejection strategy of the specified thread pool. This rejection strategy will be used when the task of the thread pool has been stored in the cache queue workQueue and cannot create a new thread to perform this task. It belongs to a current limiting protection mechanism.

The workflow of the thread pool starts with its execution method  execute (). The source code is as follows:

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);
        // 如果线程池的线程数为 0 时(当 corePoolSize 设置为 0 时会发生)
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false); // 新建线程执行任务
    }
    // 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
    else if (!addWorker(command, false)) 
        // 执行拒绝策略
        reject(command);
}

The parameter description of the addWorker(Runnable firstTask, boolean core) method is as follows:

  • firstTask , the task that the thread should run first, if not, it can be set to null;
  • core , the threshold (maximum value) for judging whether a thread can be created. If it is true, it means using corePoolSize as the threshold, and false means using maximumPoolSize as the threshold.

2. Test site analysis

This interview question examines your mastery of thread pool and ThreadPoolExecutor. It also belongs to the basic knowledge of Java. Almost all interviews will be asked. The main flow of thread pool task execution can be referred to the following flowchart: 

There are also the following interview questions related to ThreadPoolExecutor:

  • How many execution methods of ThreadPoolExecutor are there? What is the difference between them?
  • What is the denial policy of threads?
  • What are the classifications of rejection policies?
  • How to customize the rejection policy?
  • Can ThreadPoolExecutor be extended? How to achieve expansion?

3. Knowledge expansion

3.1 execute() VS submit()

Both execute() and submit() are used to execute thread pool tasks. The main difference between them is that the submit() method can receive the return value of the thread pool execution, while execute() cannot receive the return value.

Look at the specific use of the two methods:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10L,
        TimeUnit.SECONDS, new LinkedBlockingQueue(20));
// execute 使用
executor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, execute.");
    }
});
// submit 使用
Future<String> future = executor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("Hello, submit.");
        return "Success";
    }
});
System.out.println(future.get());

The execution results of the above program are as follows:

Hello, submit.
Hello, execute.
Success

From the above results, we can see that the submit() method can cooperate with Futrue to receive the return value of thread execution. Another difference between them is that the execute() method belongs to the method of the Executor interface, while the submit() method is a method of the ExecutorService interface. Their inheritance relationship is shown in the following figure:

3.2 Rejection strategy of thread pool

When the task queue in the thread pool is full, and when a task is added, it will first determine whether the number of threads in the current thread pool is greater than or equal to the maximum value of the thread pool, and if it is, the thread pool’s rejection policy will be triggered.

There are 4 types of rejection strategies that Java comes with:

  • AbortPolicy , terminate the policy, the thread pool will throw an exception and terminate the execution, it is the default rejection policy;
  • CallerRunsPolicy , hand over the task to the current thread for execution;
  • DiscardPolicy , ignore this task (the latest task);
  • DiscardOldestPolicy ignores the oldest task (the first task added to the queue).
     

 For example, let's demonstrate a rejection policy of AbortPolicy, the code is as follows:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new ThreadPoolExecutor.AbortPolicy()); // 添加 AbortPolicy 拒绝策略
for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}

The execution result of the above program:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.lagou.interview.ThreadPoolExample$$Lambda$1/1096979270@448139f0 rejected from java.util.concurrent.ThreadPoolExecutor@7cca494b[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
 at com.lagou.interview.ThreadPoolExample.rejected(ThreadPoolExample.java:35)
 at com.lagou.interview.ThreadPoolExample.main(ThreadPoolExample.java:26)

It can be seen that when the sixth task came, the thread pool executed the AbortPolicy rejection strategy and threw an exception. Because the queue stores up to 2 tasks, and up to 3 threads can be created to execute tasks (2+3=5), when the 6th task comes, the thread pool is too busy.

3.3 Customized rejection policy

To customize the rejection policy, you only need to create a RejectedExecutionHandler object, and then rewrite its rejectedExecution() method, as shown in the following code:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new RejectedExecutionHandler() {  // 添加自定义拒绝策略
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                // 业务处理方法
                System.out.println("执行自定义拒绝策略");
            }
        });
for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}

The results of the above code execution are as follows:

执行自定义拒绝策略
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2

It can be seen that the thread pool has implemented a custom rejection strategy, and we can add our own business processing code in rejectedExecution.

3.4 ThreadPoolExecutor extension

The extension of ThreadPoolExecutor is mainly achieved by rewriting its beforeExecute() and afterExecute() methods. We can add logs to the extension method or implement data statistics, such as counting the execution time of threads, as shown in the following code:

public class ThreadPoolExtend {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程池扩展调用
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(2, 4, 10,
                TimeUnit.SECONDS, new LinkedBlockingQueue());
        for (int i = 0; i < 3; i++) {
            executor.execute(() -> {
                Thread.currentThread().getName();
            });
        }
    }
   /**
     * 线程池扩展
     */
    static class MyThreadPoolExecutor extends ThreadPoolExecutor {
        // 保存线程执行开始时间
        private final ThreadLocal<Long> localTime = new ThreadLocal<>();
        public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
        /**
         * 开始执行之前
         * @param t 线程
         * @param r 任务
         */
        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            Long sTime = System.nanoTime(); // 开始时间 (单位:纳秒)
            localTime.set(sTime);
            System.out.println(String.format("%s | before | time=%s",
                    t.getName(), sTime));
            super.beforeExecute(t, r);
        }
        /**
         * 执行完成之后
         * @param r 任务
         * @param t 抛出的异常
         */
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            Long eTime = System.nanoTime(); // 结束时间 (单位:纳秒)
            Long totalTime = eTime - localTime.get(); // 执行总时间
            System.out.println(String.format("%s | after | time=%s | 耗时:%s 毫秒",
                    Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));
            super.afterExecute(r, t);
        }
    }
}

The execution result of the above program is as follows:

pool-1-thread-1 | before | time=4570298843700
pool-1-thread-2 | before | time=4570298840000
pool-1-thread-1 | after | time=4570327059500 | 耗时:28.2158 毫秒
pool-1-thread-2 | after | time=4570327138100 | 耗时:28.2981 毫秒
pool-1-thread-1 | before | time=4570328467800
pool-1-thread-1 | after | time=4570328636800 | 耗时:0.169 毫秒

4. Summary

The use of the thread pool must be created by ThreadPoolExecutor, so that the operating rules of the thread pool can be more clearly defined and the risk of resource exhaustion can be avoided. At the same time, it also introduces the seven core parameters of ThreadPoolExecutor, including the difference between the number of core threads and the maximum number of threads. When the task queue of the thread pool has no free space and the number of threads in the thread pool has reached the maximum number of threads, it will Execution rejection strategy, Java has 4 automatic rejection strategies. Users can also customize rejection strategy by overriding rejectedExecution(). We can also overwrite beforeExecute() and afterExecute() to realize the extended functions of ThreadPoolExecutor.

Guess you like

Origin blog.csdn.net/lsx2017/article/details/114005690