[Java study notes - concurrent programming] thread pool

foreword

In the previous article, we sorted out the concepts of Java threads and tasks. This article talks about the Java thread pool. While we understand the thread pool better, we also add some design ideas.

Portal:

[Java Study Notes - Basics] About Java Multithreading - Threads and Tasks

1. The emergence of Java thread pool

If we now need to optimize the performance of concurrent scenarios, where should we start? Obviously, the simplest angle: save the overhead of thread creation and destruction .

Whether it is an IO-intensive application or a computing-intensive application, the management of threads requires cost, so from the perspective of the computer, we should also limit number of threads . The purpose of creating threads is to make the application have better performance, and if there are too many suspended threads, the cost of thread management is too high, and the performance of the application will also decrease.

For the above two requirements, the thread pool was born.

First, let me show you the relationship between the interface and the implementation class in this article:

insert image description here

Second, the basic interface of the Java thread pool

Executor 与 ExecutorService

This is the basic interface of the two thread pools, and ExecutorService inherits Executor. In general, there are more implementations using ExecutorService because it has a more complete specification. Take a look at these two interfaces first:

public interface Executor {
    
    

    void execute(Runnable command);
    
}

Similar to the Runnable interface, the definition is very simple, put the task implementation into the executor, and execute the task.

public interface ExecutorService extends Executor {
    
    

	//关闭控制类函数
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

	//运行类函数
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
	
	//批量运行类函数
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Here, we can see the prototype of the thread pool. In order to better manage threads, we can realize the basic functions required by the thread pool.

Although ExecutorService has more specifications than Executor, the summary is nothing more than three categories:

  • Shutdown and other control functions (pool level)
  • Run class functions (thread level)
  • Batch operation type function (extension of the second type)

Here we can pay attention to these two interfaces:

    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);

Does it feel familiar? Press the button here first, and you will know why it is designed this way in the next section.

3. Basic implementation of Java thread pool

AbstractExecutorService

This abstract class implements the ExecutorService interface in stages. In this class, some ideas and steps for implementing the thread pool are clarified.

First, let’s take a look at the basic implementation of the task and the execution of a single task:

  • The basic task type of the thread pool is FutureTask
  • Submitting a task function requires two steps: create a new task object -> the executor executes the task.
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    
    
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    
    
        return new FutureTask<T>(callable);
    }

    public Future<?> submit(Runnable task) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Runnable task, T result) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

So far, we understand why the submit function looks so familiar, because it follows the same logic as FutureTask, so it also provides two neat interfaces.

Next, let's take a look at the processing method of batch operation. invokeAny involves the scheduling of thread management. Here, let's press the table first. Let's first look at the implementation in invokeAll:

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    
    
        if (tasks == null)
            throw new NullPointerException();
        ArrayList<Future<T>> futures = new ArrayList<>(tasks.size());
        try {
    
    
            for (Callable<T> t : tasks) {
    
    
                RunnableFuture<T> f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++) {
    
    
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
    
    
                    try {
    
     f.get(); }
                    catch (CancellationException | ExecutionException ignore) {
    
    }
                }
            }
            return futures;
        } catch (Throwable t) {
    
    
            cancelAll(futures);
            throw t;
        }
    }

It can be seen that fetching values ​​is a process of traversing the entire list, because the results are obtained in order of the list, so the completion time is determined by the slowest task .

So far, we have a preliminary understanding of the thread pool, let's take a look at how Java implements a complete thread pool.

ThreadPoolExecutor

First of all, we can take a look at the minimum elements required for the initialization of the thread pool through the construction method .

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

Parameter Description:

  • corePoolSize: The number of core threads in the thread pool. When adding a task, if the current number of threads (idle + non-idle) is less than corePoolSize, even if there are currently idle threads, new threads will be created to execute the task. If the current number of threads (idle + non-idle) is equal to corePoolSize, no more threads are recreated.
  • maximumPoolSize: The maximum number of threads in the thread pool. If the blocking queue is full and the current number of threads (idle + non-idle) is less than or equal to maximumPoolSize, a new thread will be created to perform the task.
  • keepAliveTime: If the current number of threads (idle + non-idle) is greater than corePoolSize, and the idle time is greater than keepAliveTime, these idle threads will be destroyed and resources will be released.
  • unit: The unit of time.
  • workQueue: An instance of a blocking queue.
  • ThreadFactory: thread factory class, generally use the default factory.
  • RejectedExecutionHandler: saturation strategy. When there are too many tasks, the processing strategy for the tasks.
1. The state of the thread pool - bit operation

In order to improve the efficiency of operation and reduce overhead, when we record some attribute values, we will use bit operations, that is, bit operations. (For example, if you want to use the Huffman tree to achieve compression in the algorithm class, you need to record various attribute data of the source file).

If you are not clear about concepts such as bit operations, you can move to:

Detailed explanation of bit operation and shift operation in java

Let's take a look at how ThreadPollExecutor uses an int to record two attributes:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //能存放 32-3 位的活跃线程数量。
    private static final int COUNT_BITS = Integer.SIZE - 3;
    // 
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits(3 bits free)
    // 在高位存储线程池状态
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    private static int runStateOf(int c)     {
    
     return c & ~COUNT_MASK; }
    private static int workerCountOf(int c)  {
    
     return c & COUNT_MASK; }
    private static int ctlOf(int rs, int wc) {
    
     return rs | wc; }
2. Production consumption model - BlockingQueue (blocking queue)

In ThreadPoolExecutor, the data structure that manages saving tasks is the blocking queue. Among them, the way of production is offer, and the way of consumption is poll and take.

public interface BlockingQueue<E> extends Queue<E> {
    
    

	//……………………

    boolean offer(E e);

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
	//如果取出元素后,队列为空,一直阻塞等待。
    E take() throws InterruptedException;
    //如果取出元素后,队列为空,则阻塞超时后返回null。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

	//……………………
	
}

In the interface specification, it is easy to see the difference between poll and take. Take a look at the application in the thread pool:

    public void execute(Runnable command) {
    
    
		//……………………
        //向队列生产任务
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            //……………………
        }
        //……………………
    }

    private Runnable getTask() {
    
    
			//……………………
			boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            try {
    
    
            //依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
                Runnable r = timed ?
                	//如果queue is null,即没有可执行任务,则阻塞超时后 return null。
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
    
    
                timedOut = false;
            }
        }
    }

Tips:
Choose the way to consume tasks based on whether the element has the timed attribute.
If it is a poll consumption method, the element has a timeout attribute (see the source code of BlockingQueue for details), and it will block at the value until the timeout (idle) time is reached, and return null. In the runWorker() function, call the thread destruction method processWorkerExit , so as to achieve: when the number of threads > corePoolSize, idle threads are recycled on time.
The take consumption method will always block and wait. For getTask(), there is always a return value, that is, the thread will not be destroyed. So as to achieve: when the number of threads <= corePoolSize, idle threads will not be recycled.

3. Task executor - Worker

In ThreadPoolExecutor, there is an inner class of Worker. Simply put, it is the container of tasks (implementation of runnable - task), which can be roughly understood as a thread, but not exactly the same.

What is Worker

In fact, Worker is a task container, and its construction method is very simple:

    private final class Worker extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
        Worker(Runnable firstTask) {
    
    
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
    }

Create a new thread object and add a task to it.

How Executor manages Worker

First of all, Executor stores Workers through a set to ensure the uniqueness of each worker.

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

Secondly, for the thread pool, the most important thing is addWorker and runWorker. I won’t post the source code here, but briefly talk about the difference and usage of these two functions.

The addWorker() function is where the Worker instance is actually created and the thread is started , because the thread.start() function is found in it. After a little search, we can see that addWorker is called in the execute() function, so we can know that if the thread pool wants to execute tasks, it needs to call the execute() function.

The runWorker() function is actually the run of the Worker (supplementary explanation).

4. Execution function - execute

The code here reveals the processing strategy of the thread pool thread pool when the number of threads is different.

insert image description here
The above picture is taken from: https://thinkwon.blog.csdn.net/article/details/102541900

    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);
        }

		//阻塞队列满,新建线程执行任务知道 max,如果创建失败或任务超过max,则启用饱和策略——中断。
        else if (!addWorker(command, false))
            reject(command);
    }
5. Saturation strategy - handler

The saturation strategy is used to deal with the current task when the thread is abnormal and the number of threads is full. You can understand a little bit.

  • AbortPolicy: Abort the execution of the submitted task and throw a RejectedExecutionException; this is the default policy in the thread pool.
    	//默认饱和策略
    	private static final RejectedExecutionHandler defaultHandler =
        	new AbortPolicy();

		//实现在池子中的内部类
	    public static class AbortPolicy implements RejectedExecutionHandler {
    
    

        public AbortPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  • CallerRunsPolicy: Use the thread of the caller to execute the task, that is, from the perspective of the caller, this task is serialized in the main thread of the caller.
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
    
    

        public CallerRunsPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                r.run();
            }
        }
    }
  • DiscardPolicy: directly discard tasks without processing (execute empty functions);
    public static class DiscardPolicy implements RejectedExecutionHandler {
    
    
    
        public DiscardPolicy() {
    
     }
        
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        
        }
    }
  • DiscardOldestPolicy: Discard the head task in the blocking queue (that is, the task with the longest storage time), and execute the current task.
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    
    

        public DiscardOldestPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

Tips:
In fact, Java's saturation strategy can be regarded as an introduction. In business scenarios, you can customize the saturation strategy for specific business functions. That's the key.

4. Why use thread pool

After the above understanding, we can summarize the value of the thread pool:

  • Reduce resource consumption. Reduce the resource consumption of creating and destroying threads by reusing existing threads;
  • Improve thread manageability. Threads are scarce resources and cannot be multiplied indefinitely. Too many threads will also burden the computer.

Guess you like

Origin blog.csdn.net/weixin_43742184/article/details/113736775