Concurrent programming (5): Implementation of thread pool and underlying principles

One: what is the thread pool

 

Pooling technology: reduce the consumption of resources every time, improve the utilization of resources. For example, thread pool, database connection pool, Http connection pool, etc.

Benefits of using thread pool:

  • Reduce resource consumption. Reduce the consumption caused by thread creation and destruction by reusing already created threads.
  • Improve response speed. When the task arrives, the task can be executed immediately without waiting for the thread to be created.
  • Improve thread manageability. Threads are scarce resources. If unlimited creation, not only will consume system resources, but also reduce the stability of the system, the use of thread pool can be a unified allocation, tuning and monitoring.

 

Two: the difference between implementing Runnable interface and Callable interface

 

Callable interface: The thread execution result will be returned or a check exception will be thrown. The thread execution result and exception detection need to use the Callable interface, otherwise the Runnable interface (code is more concise).

Runnable has existed since Java 1.0, but Callable was only introduced in Java 1.5, the purpose is to deal with the use cases that Runnable does not support. The Runnable interface does not return results or throws a check exception, but the Callable interface does. Therefore, if the task does not need to return results or throw an exception, it is recommended to use the Runnable interface, so that the code looks more concise.

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     * 
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
...

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result 计算结果
     * @throws Exception if unable to compute a result 无法计算结果则抛出异常
     */
    V call() throws Exception;
}
...
// FutureTask类 对Callable进行了封装
public class FutureTask<V> implements RunnableFuture<V> {
    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    private static final int NEW          = 0;
    
    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

}

Three: What is the difference between executing execute () method and submit () method?

  1. execute () method: used to submit tasks that do not require a return value, so it is impossible to determine whether the task was successfully executed by the thread pool;
  2. The submit () method: used to submit tasks that require a return value. The thread pool will return an object of type Future. Through this Future object, you can determine whether the task is successfully executed, and you can get the return value through the Future's get () method. The get () method will block the current thread until the task is completed, and use get (Long timeout, TimeUnit unit) method will block the current thread and return immediately after a period of time, at this time there may be tasks that have not been completed.

submit method:

// AbstractExecutorService 接口中的一个 submit 方法
public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
 }
 ...
 protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
 }

execute method:

// execute 方法
public void execute(Runnable command) {
      ...
 }

Four: how to create a thread pool

 

In the "Alibaba Java Development Manual", the mandatory thread pool does not allow the creation of Executors, but through the ThreadPoolExecutor .

The disadvantages of Executors returning thread pool objects are as follows:

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

1: Through the Executors tool class of the Executor framework to achieve We can create three types of ThreadPoolExecutor:

  • FixedThreadPool: This method returns a thread pool with a fixed number of threads. The number of threads in this thread pool is always the same. When a new task is submitted, if there is an idle thread in the thread pool, it will be executed immediately. If not, the new task will be temporarily stored in a task queue, and when the thread is idle, the task in the task queue will be processed.
  • SingleThreadExecutor: The method returns a thread pool with only one thread. If more than one task is submitted to the thread pool, the task will be saved in a task queue, wait for the thread to be idle, and execute the tasks in the queue in first-in first-out order.
  • CachedThreadPool: This method returns a thread pool that can adjust the number of threads according to the actual situation. The number of threads in the thread pool is uncertain, but if there are idle threads that can be reused, reusable threads are given priority. If all threads are working and a new task is submitted, a new thread will be created to process the task. After the execution of the current task is completed, all threads will return to the thread pool for reuse.

2: ThreadPoolExecutor construction method (all parameters):

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters and default thread factory.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code handler} is null
 */
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;
}

Important parameters:

corePoolSize: number of core threads (minimum number of threads that can run simultaneously)

maximumPoolSize: When the tasks stored in the queue reach the queue capacity, the current number of threads that can run simultaneously becomes the maximum number of threads.

workQueue: When a new task comes, it will first determine whether the number of currently running threads reaches the number of core threads. If so, the new task will be stored in the queue.

Common parameters:

keepAliveTime: When the number of threads in the thread pool is greater than corePoolSize, if there are no new tasks submitted at this time, the threads outside the core thread will not be destroyed immediately, but will wait until the waiting time exceeds keepAliveTime will be recycled ;

unit: The time unit of the keepAliveTime parameter.

threadFactory: executor will be used when creating a new thread.

handler: Saturation strategy.

 

 Five: ThreadPoolExecutor saturation strategy

The processing method used when the number of currently running threads reaches the maximum number of threads and the queue is already full.

Four saturation strategies:

  • ThreadPoolExecutor.AbortPolicy: Throw RejectedExecutionException to reject the processing of new tasks.
  • ThreadPoolExecutor.CallerRunsPolicy: call to execute its own thread running task. You will not terminate the request. But this strategy will reduce the speed of submitting new tasks and affect the overall performance of the program. In addition, this strategy likes to increase the queue capacity. If your application can tolerate this delay and you cannot discard any task request, you can choose this strategy.
  • ThreadPoolExecutor.DiscardPolicy: Discard the new task without processing it.
  • ThreadPoolExecutor.DiscardOldestPolicy: This policy will discard the earliest unprocessed task request.

Six: a simple thread pool Demo: Runnable + ThreadPoolExecutor

public class DemoTest {
    private static int corePoolSize = 5;//核心线程数
    private static int maximumPoolSize = 15;//最大线程数
    private static long keepAliveTime = 1l;//线程未能获取最大请求时间,超过则销毁
    private static BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);//线程存放队列

    static class MyRunnable implements Runnable {

        private String name;
        public MyRunnable(String name){
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"开始--->"+System.currentTimeMillis());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"结束--->"+System.currentTimeMillis());
        }
    }

    public static void main(String[] args){
        ThreadPoolExecutor threadPoolExecutor = 
        new ThreadPoolExecutor(corePoolSize,maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                workQueue,new ThreadPoolExecutor.CallerRunsPolicy());
                //线程饱和策略
        for (int i = 0; i < 15; i++) {
            Runnable myRunnable = new MyRunnable(""+i);
            // 线程池中调用线程
            threadPoolExecutor.execute(myRunnable);
        }
        // 终止线程池
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()){

        }
        System.out.println("Finished all threads");
    }
}
  1. corePoolSize: The number of core threads is 5.
  2. maximumPoolSize: maximum number of threads 10
  3. keepAliveTime: The waiting time is 1L.
  4. unit: The unit of the waiting time is TimeUnit.SECONDS.
  5. workQueue: The task queue is ArrayBlockingQueue, and the capacity is 100;
  6. handler: The saturation strategy is CallerRunsPolicy

How to set the number of core threads:

  • CPU-intensive tasks (N + 1): This task consumes mainly CPU resources. You can set the number of threads to N (number of CPU cores) +1, and one more thread than the number of CPU cores is to prevent sporadic threads. Page fault interruption, or other reasons caused by the suspension of the task. Once the task is suspended, the CPU will be idle, and in this case an extra thread can make full use of the CPU's idle time.
  • I / O-intensive tasks (2N): When this task is applied, the system will spend most of its time processing I / O interactions, and threads will not use CPU to process I / O during the time period. You can hand over the CPU to other threads. Therefore, in the application of I / O intensive tasks, we can configure more threads, the specific calculation method is 2N

Seven: Principle analysis of thread pool

The source code analysis is as follows:

// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int workerCountOf(int c) {
    return c & CAPACITY;
}

private final BlockingQueue<Runnable> workQueue;

public void execute(Runnable command) {
    // 如果任务为null,则抛出异常。
    if (command == null)
        throw new NullPointerException();
    // ctl 中保存的线程池当前的一些状态信息
    int c = ctl.get();

    //  下面会涉及到 3 步 操作
    // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
    // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
    // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
        if (!isRunning(recheck) && remove(command))
            reject(command);
            // 如果当前线程池为空就新创建一个线程并执行。
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
    else if (!addWorker(command, false))
        reject(command);
}

Eight: Detailed explanation of several common thread pools

 

 FixedThreadPool can reuse a fixed number of thread pool

  1. If the number of currently running threads is less than corePoolSize, if a new task comes again, a new thread is created to execute the task;
  2. After the current number of running threads is equal to corePoolSize, if a new task comes, the task will be added to LinkedBlockingQueue;
  3. After the thread in the thread pool executes the task at hand, it will repeatedly obtain the task from the LinkedBlockingQueue in the loop to execute;

Disadvantages:

FixedThreadPool uses the unbounded queue LinkedBlockingQueue (the capacity of the queue is Intger.MAX_VALUE) as the work queue of the thread pool, which will have the following effects on the thread pool:

  1. When the number of threads in the thread pool reaches corePoolSize, new tasks will wait in the unbounded queue, so the number of threads in the thread pool will not exceed corePoolSize;
  2. Because the maximumPoolSize will be an invalid parameter when using an unbounded queue, because it is impossible to have a task queue full. Therefore, by creating the source code of FixedThreadPool, it can be seen that the corePoolSize and maximumPoolSize of the FixedThreadPool created are set to the same value.
  3. Due to 1 and 2, keepAliveTime will be an invalid parameter when using unbounded queues;
  4. FixedThreadPool (not executing shutdown () or shutdownNow ()) in operation will not reject the task, and will cause OOM (memory overflow) when there are more tasks.

 

SingleThreadExecutor thread pool with only one thread

SingleThreadExecutor uses the unbounded queue LinkedBlockingQueue as the work queue of the thread pool (the capacity of the queue is Intger.MAX_VALUE). SingleThreadExecutor uses an unbounded queue as the work queue of the thread pool will have the same impact on the thread pool as FixedThreadPool. To put it simply, it may cause OOM,

 

CachedThreadPool is a thread pool that creates new threads as needed

The number of threads allowed by CachedThreadPool is Integer.MAX_VALUE, which may create a large number of threads, resulting in OOM.

 

ScheduledThreadPool scheduledThreadPoolExecutor is mainly used to run tasks after a given delay, or to execute tasks on a regular basis

The task queue used by ScheduledThreadPoolExecutor DelayQueue (delay queue) encapsulates a PriorityQueue, PriorityQueue (priority queue) will sort the tasks in the queue, the execution time is short and the first is executed first (ScheduledFutureTask's time variable is small and executed first ), If the execution time is the same, the task submitted first will be executed first (ScheduledFutureTask whose squenceNumber variable is small is executed first).

 

Comparison of ScheduledThreadPoolExecutor and Timer:

  • Timer is sensitive to changes in system clock, ScheduledThreadPoolExecutor is not;
  • Timer has only one thread of execution, so long-running tasks can delay other tasks. ScheduledThreadPoolExecutor can configure any number of threads. In addition, if you want (by providing a ThreadFactory), you have full control over the threads created;
  • Runtime exceptions thrown in TimerTask will kill a thread and cause Timer to crash :-( ... ie scheduled tasks will no longer run. ScheduledThreadExecutor not only catches runtime exceptions, but also allows you to handle them when needed (via Override afterExecute method ThreadPoolExecutor). The task that throws an exception will be canceled, but other tasks will continue to run.

Dear viewers, welcome to like comments, discuss and learn together, and bloggers will also update relevant knowledge about concurrent programming in time! At the same time, the blogger is also a veteran fan, programming, second-dimension, appreciation all the way, getting better all the way.

 

 

Published 28 original articles · praised 0 · visits 9931

Guess you like

Origin blog.csdn.net/weixin_38246518/article/details/105590892