java Concurrent Programming - thread pool parsing

Posted by lopes_andre on Thu, 23 Dec 2021 16:52:42 +0100

The previous article described how to create a thread. This is relatively simple. There will be a problem. If more threads are created, the efficiency of the system will be greatly reduced, because it takes time to create and destroy threads frequently

So how to reuse existing threads? That is to achieve this effect through thread pool!

Firstly, we start with the method in the core ThreadPoolExecutor class, then describe its implementation principle, then give its use example, and finally discuss how to reasonably configure the size of thread pool.

1, ThreadPoolExecutor class

ThreadPoolExecutor is Java uitl. The classes under concurrent (JUC java concurrency toolkit for short) are the core classes in the thread pool.

Four constructors are provided in the ThreadPoolExecutor class:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

1. Meaning of parameters

1, corePoolSize (basic size of thread pool): when submitting a task to the thread pool, if the number of threads created by the thread pool is less than corePoolSize, even if there are idle threads at this time, a new thread will be created to execute the task until the number of threads created is greater than or equal to corePoolSize (in addition to submitting a new task to create and start threads) (constructed on demand), you can also start the basic threads in the thread pool in advance through the prestartCoreThread() or prestartAllCoreThreads() methods.)

2. maximumPoolSize: the maximum number of threads allowed by the thread pool. When the queue is full and the number of threads created is less than maximumPoolSize, the thread pool will create new threads to execute tasks. In addition, for unbounded queues, this parameter can be ignored.

3. keepAliveTime: when the number of threads in the thread pool is greater than the number of core threads, if the idle time of the thread exceeds the thread survival time, the thread will be destroyed until the number of threads in the thread pool is less than or equal to the number of core threads.

4. Unit: the time unit of the parameter keepAliveTime. There are seven values. In the TimeUnit class, there are seven static attributes:

TimeUnit.DAYS; / / days
TimeUnit.HOURS; / / hours
TimeUnit.MINUTES; / / minutes
TimeUnit.SECONDS; / / seconds
TimeUnit.MILLISECONDS; / / MS
TimeUnit.MICROSECONDS; / / subtle
TimeUnit.NANOSECONDS; / / nanosecond

5. workQueue: a blocking queue used to transmit and save tasks waiting to be executed.

General queues have the following options:

ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

6. threadFactory (thread factory): used to create new threads. Threads created by threadFactory also adopt the method of new Thread(). Thread names created by threadFactory have a unified style: pool-m-thread-n (M is the number of thread pool and N is the number of threads in thread pool).

7. handler (thread saturation Policy): when the thread pool and queue are full, adding a thread will execute this policy.

There are generally the following types of rejection policies. Of course, you can also customize the rejection policy (implement the RejectedExecutionHandler interface):

ThreadPoolExecutor.AbortPolicy: discards the task and throws RejectedExecutionException. 
ThreadPoolExecutor.DiscardPolicy: also discards tasks without throwing exceptions. 
ThreadPoolExecutor.DiscardOldestPolicy: discard the task at the top of the queue, and then try to execute the task again (repeat this process)
ThreadPoolExecutor.CallerRunsPolicy: this task is handled by the calling thread 

2. Structural analysis

ThreadPoolExecutor inherits AbstractExecutorService

AbstractExecutorService implements the ExecutorService interface

ExecutorService inherits Executor

public interface Executor {
    void execute(Runnable command);
}
public interface ExecutorService extends Executor {
 
    void shutdown();
    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;
}

The execute() method is actually a method declared in the Executor. It is specifically implemented in ThreadPoolExecutor. This method is the core method of ThreadPoolExecutor. Through this method, you can submit a task to the thread pool for execution.

The submit() method is a method declared in ExecutorService. It has been implemented in AbstractExecutorService. It is not rewritten in ThreadPoolExecutor. This method is also used to submit tasks to the thread pool, but it is different from the execute() method. It can return the results of task execution. See the implementation of the submit() method, You will find that it is actually the called execute() method, but it uses Future to get the task execution results

2, Schematic diagram of thread pool implementation

1. Flow chart

2. Execution diagram

3, Implementation principle of thread pool

1. Thread pool life cycle

A volatile variable is defined in the ThreadPoolExecutor, and several static final variables are defined to represent the stages of each life cycle of the thread pool:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
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;
private static int ctlOf(int rs, int wc) { return rs | wc; }

runState indicates the state of the current thread pool. It is a volatile variable to ensure the visibility between threads;

  • RUNNING
    This is the initial state of the thread pool. In this state, the thread pool will accept new tasks and process the tasks waiting in the queue.
  • SHUTDOWN
    The RUNNING state enters this state after calling the shutdown method. In this state, the thread pool does not accept new tasks, but will process the tasks waiting in the queue.
  • STOP
    The RUNNING/SHUTDOWN state enters this state after calling the shutdown now method. In this state, the thread pool does not accept new tasks or handle existing waiting tasks, and will interrupt existing running threads.
  • TIDYING
    SHUTDOWN/STOP status will flow to this status. At this time, all tasks have been run, the number of worker threads is 0, and the task queue is empty. Literally, the thread pool has been cleared.
  • TERMINATED
    In the TIDYING state, the thread pool enters this state after executing the terminated hook method. At this time, the thread pool has been completely terminated.

2. Member variable of ThreadPoolExecutor

private final BlockingQueue<Runnable> workQueue;              //The task cache queue is used to store tasks waiting to be executed
private final ReentrantLock mainLock = new ReentrantLock();   //The main state lock of the thread pool, which controls the state of the thread pool (such as the size of the thread pool)
                                                              //, runState, etc.)
private final HashSet<Worker> workers = new HashSet<Worker>();  //Used to store worksets
 
private volatile long  keepAliveTime;    //Thread inventory time   
private volatile boolean allowCoreThreadTimeOut;   //Whether to allow setting the survival time for core threads
private volatile int   corePoolSize;     //The size of the core pool (that is, when the number of threads in the thread pool is greater than this parameter, the submitted tasks will be put into the task cache queue)
private volatile int   maximumPoolSize;   //The maximum number of threads that the thread pool can tolerate
 
private volatile int   poolSize;       //The current number of threads in the thread pool
 
private volatile RejectedExecutionHandler handler; //Task rejection policy
 
private volatile ThreadFactory threadFactory;   //Thread factory, used to create threads
 
private int largestPoolSize;   //Used to record the maximum number of threads in the thread pool
 
private long completedTaskCount;   //Used to record the number of completed tasks

3. execute() method

In the ThreadPoolExecutor class, the core task submission method is the execute() method. Although tasks can be submitted through submit, in fact, the final call in the submit method is the execute() method, so we only need to study the implementation principle of the execute() method:

/**
 * execute Method can be said to be the core method in the thread pool,
 * In the AbstractExecutorService at the upper level of the inheritance chain, various methods for accepting new tasks are finally forwarded to this method for task processing.
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Classified discussion:
     * 1. If the current number of threads is less than the number of core threads, a new thread will be started to execute the submitted task.
     *
     * 2. Attempt to add a task to the task queue. At this time, you need to check the gap from the beginning of the method to the current time again,
     *    Whether the thread pool is closed / there are no worker threads in the thread pool.
     *    If the thread pool has been closed, you need to remove the previously submitted tasks from the task queue.
     *    If there are no worker threads, you need to add an empty task worker thread to execute the submitted task.
     *
     * 3. If a task cannot be added to the blocking queue, try to create a new thread to execute the task.
     *    If it fails, the callback saturates the policy processing task.
     */
    int c = ctl.get();
    // Number of threads < corepoolsize
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // Check whether the thread pool is running and add tasks to the task queue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        /*
         * Check again whether the thread pool is running. If not, remove the task and call back the saturation policy to reject the task.
         * Because it is possible that the above if condition reads that the thread pool is running, and then the shutdown / shutdown now method is called,
         * At this time, you need to remove the task that has just been added to the task queue.
         */
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // If the workerCount is 0, you need to add a worker thread to execute the submitted task
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    /*
     *  Add a new worker thread to handle the task.
     *  If it fails, it indicates that the thread pool has been closed or saturated. At this time, the saturation policy is called back to reject the task.
     */
    else if (!addWorker(command, false))
        reject(command);
}

4. addWorker method

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        /*
         * If the thread pool status is at least STOP, false is returned and the task is not accepted.
         * If the thread pool status is SHUTDOWN, and the firstTask is not null or the task queue is empty, the task is also not accepted.
         */
        if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                    firstTask == null &&
                    ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            /*
             * CAPACITY Is (1 < < 29) - 1, which is the real upper bound of the number of threads in the thread pool and must not be exceeded.
             * Because the ThreadPoolExecutor is designed to use the lower 29 bits to represent the number of working threads.
             *
             * Otherwise, judge according to whether corePoolSize is taken as the upper bound in the parameter. If it exceeds, adding a worker fails.
             */
            if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // Add workCount successfully, jump out of the whole cycle and go down.
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();
            /* 
             * Reread the general control status. If the operation status changes, retry the whole large cycle.
             * Otherwise, the workCount has changed. Retry the inner loop.
             */
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    // When running here, the number of threads in the thread pool has been successfully + 1. Proceed to the substantive operation below.

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Since the state of the process pool for obtaining locks may have changed, you need to read the state again.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // Add a new worker to the worker thread collection and update largestPoolSize.
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // After the worker is successfully added, start the worker thread.
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // If the worker thread does not start successfully, roll back the changes of the worker set and the worker counter.
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

5,addWorkerFailed

When adding a worker thread fails, call addWorkerFailed:

  1. Delete the failed worker from the worker collection.
  2. workCount minus 1.
  3. A call to tryTerminate attempts to terminate the thread pool.
private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (w != null)
            workers.remove(w);
        decrementWorkerCount();
        tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

6. Implementation of Worker class

private final class Worker implements Runnable {
    private final ReentrantLock runLock = new ReentrantLock();
    private Runnable firstTask;
    volatile long completedTasks;
    Thread thread;
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
    }
    boolean isActive() {
        return runLock.isLocked();
    }
    void interruptIfIdle() {
        final ReentrantLock runLock = this.runLock;
        if (runLock.tryLock()) {
            try {
        if (thread != Thread.currentThread())
        thread.interrupt();
            } finally {
                runLock.unlock();
            }
        }
    }
    void interruptNow() {
        thread.interrupt();
    }
 
   /**
 * Worker threads run core logic.
 * Simply put, what you do is to keep taking tasks from the task queue and running them.
 */
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    // Setting firstTask to null is important from a GC perspective.
    w.firstTask = null;
    // Set the mutex status to 0 and it can be interrupted at this time.
    w.unlock();
    // Used to mark whether there are exceptions when completing the task.
    boolean completedAbruptly = true;
    try {
        // Loop: the initial task (the first time) or one from the blocking queue (subsequent).        
        while (task != null || (task = getTask()) != null) {
           /*
            * Get mutex.
            * When a mutex is held, calling the thread pool shutdown method does not interrupt the thread.
            * However, the shutdown now method ignores the mutex and interrupts all threads.
            */
            w.lock();
            /*
             * What if we do here is to determine whether we need to interrupt the current thread.
             * If the thread pool is at least in the STOP stage and the current thread is not interrupted, interrupt the current thread;
             * Otherwise, the break in the thread is cleared.
             *
             * if Thread in condition interrupted() &&runStateAtLeast(ctl.get(), STOP)
             * To put it bluntly, clear the interrupt bit and confirm that the current thread pool state has not reached the STOP stage.
             */  
            if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                         runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                // Call the preprocessing hook implemented by the subclass.
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // Real execution task
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    // Call the post-processing hook implemented by the subclass.
                    afterExecute(task, thrown);
                }
            } finally {
                // Clear task, counter + 1, release mutex.
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        /*
         * Processing worker thread exit.
         * The pre-processing, task calling and post-processing in the above main loop may throw exceptions.
         */
        processWorkerExit(w, completedAbruptly);
    }
}
 
    public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);   //Clean up when there are no tasks in the task queue       
        }
    }
}

It actually implements the Runnable interface, so the above thread t = threadfactory newThread(w); The effect is basically the same as that of the following sentence:

	Thread t = new Thread(w);

It is equivalent to passing in a Runnable task and executing the Runnable in the thread t.

Since the Worker implements the Runnable interface, the core method is naturally the run() method

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

It can be seen from the implementation of the run method that it first executes the task firstTask passed in through the constructor. After calling runTask() to execute the firstTask, it continues to get new tasks through getTask() in the while loop. Where to get them? Naturally, it is retrieved from the task cache queue. getTask is a method in the ThreadPoolExecutor class, not in the Worker class. The following is the implementation of getTask method:

/**
 * The core method by which a worker takes a task from a task queue.
 * Blocking or time limit acquisition is determined according to the configuration.
 * null will be returned in the following cases, and then the thread will exit (the runWorker method loop ends):
 * 1. The current number of worker threads exceeds maximumPoolSize (this is possible because maximumPoolSize can be adjusted dynamically).
 * 2. The thread pool status is STOP (because the STOP status does not process the tasks in the task queue).
 * 3. The thread pool state is SHUTDOWN and the task queue is empty (because the SHUTDOWN state still needs to process waiting tasks).
 * 4. Decide whether to exit the current worker thread according to the thread pool parameter status and whether the thread is idle for more than keepAliveTime.
 */
private Runnable getTask() {
    // Whether the last poll task from the task queue timed out.
    boolean timedOut = false;

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        /*
         * If the thread pool state is no longer RUNNING, set the number of working threads of ctl to - 1
         * if The condition is equivalent to RS > = stop | (rs = = shutdown & & workqueue. Isempty())
         */
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        /*
         * allowCoreThreadTimeOut Is used to set whether the core thread is affected by keepAliveTime.
         * When allowCoreThreadTimeOut is true or the number of worker threads > corepoolsize,
         * The current worker thread is affected by keepAliveTime.
         */
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        /*
         * 1. The number of worker threads > maximumpoolsize. The current worker thread needs to exit.
         * 2. timed && timedOut == true Indicates that the current thread is affected by keepAliveTime and the last get task timed out.
         *    In this case, you can exit as long as the current thread is not the last worker thread or the task queue is empty.
         *
         *    In other words, if the queue is not empty, the current thread cannot be the last worker thread,
         *    Otherwise, if you quit, there will be no thread to process the task.
         */ 
        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            // Set the workCount of ctl minus 1. If CAS fails, you need to retry (because the conditions in the above if may not be met).
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // According to the value of the timed variable, it is determined whether to obtain the tasks in the task queue within the time limit or blocking.
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // workQueue.take will not return null, so it indicates that the poll timed out.
            timedOut = true;
        } catch (InterruptedException retry) {
            // If it is interrupted while waiting on the blocking queue, clear the timeout ID and try the cycle again.
            timedOut = false;
        }
    }
}

7. processWorkerExit method

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    /*
     * Because of normal exit, workerCount minus 1 is done when getTask cannot get the task.
     * Therefore, in case of exceptions, you need to subtract 1 from workCount in this method.
     */
    if (completedAbruptly)
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // Accumulate completedTaskCount to remove itself from the worker thread collection.
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // Since workCount minus 1, you need to call the tryTerminate method.
    tryTerminate();

    int c = ctl.get();
    // As long as the thread pool has not reached the STOP state, the tasks in the task queue still need to be processed.
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            /* 
             * Determines the minimum number of worker threads required in the RUNNING or SHUTDOWN state.
             *
             * By default, when the core thread is not restricted,
             * In this case, the number of core threads should be stable.
             * Otherwise, no threads are allowed in the thread pool.
             */
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // If the task queue is not empty, at least 1 worker thread is required.
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            // No worker threads need to be compensated.
            if (workerCountOf(c) >= min)
                return;
        }
        // In case of abnormal exit or need to compensate a thread, add an empty task worker thread.
        addWorker(null, false);
    }
}

8. Thread pool shutdown

ThreadPoolExecutor provides two methods for closing the thread pool, namely shutdown() and shutdown now(), where:

  • shutdown(): the thread pool will not be terminated immediately, but will not be terminated until all tasks in the task cache queue have been executed, but will never accept new tasks
  • Shutdown now(): immediately terminate the thread pool, try to interrupt the executing task, empty the task cache queue, and return the unexecuted task

9. Dynamic adjustment of thread pool capacity

ThreadPoolExecutor provides methods to dynamically adjust the size of thread pool capacity: setCorePoolSize() and setMaximumPoolSize(),

  • setCorePoolSize: sets the core pool size
  • setMaximumPoolSize: sets the maximum number and size of threads that can be created by the thread pool

When the above parameters increase from small to large, ThreadPoolExecutor assigns a thread value, and may immediately create a new thread to execute the task.

10. Task rejection strategy

When the task cache queue of the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, the task rejection policy will be adopted if there are still tasks coming. Generally, there are four strategies:

ThreadPoolExecutor.AbortPolicy: discards the task and throws RejectedExecutionException.

ThreadPoolExecutor.DiscardPolicy: also discards tasks without throwing exceptions.

ThreadPoolExecutor.DiscardOldestPolicy: discard the task at the top of the queue, and then try to execute the task again (repeat this process)

ThreadPoolExecutor.CallerRunsPolicy: this task is handled by the calling thread

IV. examples

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.junit.Test;

public class ThreadPoolExecutorTest {
	
	@Test
	public void threadPoolExecutorTest(){
         ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<10;i++){
             TestTask testTask = new TestTask(i);
             executor.execute(testTask);
             System.out.println("Number of threads in the thread pool:"+executor.getPoolSize()+",Number of tasks waiting in the queue:"+
             executor.getQueue().size()+",Number of play tasks performed:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
		 
		 
		
	class TestTask implements Runnable {
	    private int taskNum;
	     
	    public TestTask(int num) {
	        this.taskNum = num;
	    }
	     
	    @Override
	    public void run() {
	        System.out.println("Executing task "+taskNum);
	        try {
	            Thread.currentThread().sleep(4000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println("task "+taskNum+"completion of enforcement");
	    }
	}
}

Test results:

Number of threads in the thread pool: 1, number of tasks waiting to be executed in the queue: 0, number of other tasks executed: 0
Number of threads in the thread pool: 2, number of tasks waiting to be executed in the queue: 0, number of other tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 0, number of other tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 1, number of tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 2, number of tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 3, number of tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 4, number of tasks executed: 0
Number of threads in the thread pool: 3, number of tasks waiting to be executed in the queue: 5, number of tasks executed: 0
Number of threads in the thread pool: 4, number of tasks waiting to be executed in the queue: 5, number of tasks executed: 0
Number of threads in the thread pool: 5, number of tasks waiting to be executed in the queue: 5, number of tasks executed: 0
Executing task 0
Executing task 1
Executing task 2
Executing task 8

It can be seen from the execution results that when the number of threads in the thread pool is greater than 3, the task will be put into the task cache queue. When the task cache queue is full, a new thread will be created. If you change the for loop to execute 20 tasks in the above program, you will throw a task rejection exception.  

However, in java doc, we are not encouraged to directly use ThreadPoolExecutor, but to use several static methods provided in Executors class to create thread pool:

Executors.newCachedThreadPool();        //Create a buffer pool with a buffer pool capacity of integer MAX_ VALUE
Executors.newSingleThreadExecutor();   //Create buffer pool with capacity of 1
Executors.newFixedThreadPool(int);    //Create a fixed size buffer pool

The following is the specific implementation of these three static methods;

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

In practice, if the three static methods provided by Executors can meet the requirements, try to use the three methods provided by Executors, because it is a little troublesome to manually configure the parameters of ThreadPoolExecutor, which should be configured according to the type and quantity of actual tasks.

The problem is that if there are too many requests, the above three methods are not applicable. The first two queues are unbounded queues. Adding them all the time will burst the memory. Finally, because it is unlimited, adding threads will also exhaust the CPU

5, How to reasonably configure the size of thread pool

1) , CPU intensive

For this task, we should try to use a smaller thread pool. Generally, the number of Cpu cores + 1

Because the CPU utilization of CPU intensive tasks is very high, if you open too many threads, you can only increase the number of thread context switches and bring additional overhead

2) , IO intensive

Method 1: you can use a large thread pool, generally the number of CPU cores * 2

The utilization rate of IO intensive CPU is not high, which allows the CPU to process other tasks while waiting for IO and make full use of CPU time

Method 2: the higher the proportion of thread waiting time, the more threads are required. The higher the percentage of thread CPU time, the fewer threads are required.

Topics: Java Multithreading Concurrent Programming thread pool