“This is the 15th day of my participation in the August More Text Challenge. For more details, see August More Text Challenge

Play with Java thread pools:

  • Play with Java thread pool 1: ThreadPoolExecutor execution process and principle
  • Play with Java thread pool 2: Use of ThreadPoolExecutor

Create a thread pool

ThreadPoolExecutor has four constructors:

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

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

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

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
Copy the code

Creating a thread pool requires the following parameters:

  • Int corePoolSize(core thread pool size) : This parameter is used to specify the size of the core thread pool. If the ThreadPoolExecutor’s prestartAllCoreThreads() method is called, the pool is created and starts all core threads in advance.

  • Int maximumPoolSize: Specifies the maximum number of threads that can be created in a thread pool. When the block queue is full, a new thread is created, and the number of threads created is less than the maximum number of threads, the thread pool will execute the task. However, if the blocking queue uses an unbounded queue, this parameter will have no effect, since blocking queues are unbounded.

  • Long keepAliveTime: The amount of time a thread in the pool can survive after it has become idle before it is destroyed. If there are many tasks and the execution time of each task is short, you can increase the keepAliveTime to improve thread utilization.

  • TimeUnit unit(keeptime unit) : TimeUnit such as hour, minute, second, millisecond, and microsecond.

  • BlockingQueue

    workQueue() : a BlockingQueue that holds tasks waiting to be executed. There are several implementations of blocking queues in the JDK:

    • ArrayBlockingQueue: Array-based bounded blocking queue. Incoming elements are sorted on a first-in, first-out (FIFO) basis.
    • LinkedBlockingQueue: A LinkedBlockingQueue based on a linked list. It is also a first-in, first-out queue. Throughput higher than ArrayBlockingQueue.
    • SynchronoutQueue: Synchronous queue, a blocking queue that does not store elements. Each insert must wait until the element is removed, otherwise the insert operation will block all the time, and the throughput is usually higher than LinkedBlockingQueue.
    • PriorityBlockingQueue: An infinite blocking queue with priority.
  • ThreadFactory ThreadFactory: you can use a ThreadFactory to give each created thread a name.

  • RejectedExecutionHandler: When both the queue and the thread pool are full, then the thread pool must use a rejecrejdexecutionHandler strategy to handle new tasks that are being submitted. The default rejection policy is AbortPolicy, which throws an exception when handling a new task. The thread pool framework in the JDK provides four denial strategies:

    • AbortPolicy: Throws an exception directly.
    • CallerRunsPolicy: The task is returned to the caller’s thread for execution.
    • DiscardOldestPolicy: Selects the most recent task in its queue and executes the current task.
    • DiscardPolicy: The task is discarded without processing.

    If these four rejection strategies are not satisfied, the RejectedExecutionHandler interface can implement its own rejection strategy.

Submit tasks to the thread pool

There are two methods to submit a task to a ThreadPoolExecutor: execute() and submit().

The difference between the two is that the execute() method only accepts Runnable types and does not return results.

The submit() method can accept both Runnable and Callable types of arguments, so the submit() method can completely replace the execute() method. The submit() method is the only way to return a return value, such as the execution result of the task. The submit() method can return an object of type Future. The return value can be obtained through the get() method of this type, which blocks the current thread until the result is returned. Using the get(Long Timeout, TimeUnit Unit) method blocks until the time ends, and if the timeout does not return a result, an error is thrown.

public class ThreadPoolExecutorDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5.10.3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10),
                Executors.defaultThreadFactory(),
                newThreadPoolExecutor.DiscardOldestPolicy() ); threadPoolExecutor.execute(() -> { System.out.println(Thread.currentThread().getName()); }); Future<String> future = threadPoolExecutor.submit(() -> Thread.currentThread().getName()); System.out.println(future.get()); }}Copy the code

Shutdown of the thread pool

You can shutdown a thread pool by calling the shutdown or shutdownNow methods of ThreadPoolExecutor. The idea is to iterate through the worker threads in the thread pool and then interrupt them one by one by calling the interrupt method on each thread, so tasks that fail to respond to interrupts may never be terminated. ShutdownNow first sets the state of the thread pool to STOP and then attempts to STOP all threads that are executing or suspending tasks and return a list of waiting tasks, whereas shutdown only sets the state of the thread pool to shutdown. Then interrupt all threads that are not executing tasks.

The isShutdown method returns true if either of the two closing methods is called. The isTerminaed method returns true when all tasks have been closed and the pool has been closed. As to which method should be called to close the thread pool, it should be determined by the nature of the task submitted to the thread pool. The shutdown method is usually called to close the thread pool, or the shutdownNow method can be called if the task does not necessarily complete.

Thread pool parameter configuration resolution

To properly configure a thread pool, you must analyze the task characteristics from the following perspectives:

  • The nature of the task: CPU intensive, IO intensive, and hybrid.
  • Priority of tasks: high, medium and low.
  • Task execution time: long, medium and short.
  • Task dependencies: Whether to rely on other system resources, such as database connections.

Tasks of different nature can be handled separately with thread pools of different sizes.

CPU intensive tasks should be configured with the smallest possible threads, such as CPU +1 thread pool.

Since the IO – intensive task threads are not always executing tasks, you should configure as many threads as possible, such as 2* CPU.

A hybrid task, if you can split it into one CPU-intensive task and one IO intensive task, will have a higher throughput than a serial one, as long as the time difference between the two tasks is not too great. If the time difference between the two tasks is too large, there is no need to break them down. You can obtain the number of cpus on the current device using runtime.geTruntime ().availableProcessors().

Tasks with different priorities can be handled using the PriorityBlockingQueue. It allows higher-priority tasks to be executed first. If higher-priority tasks are constantly being submitted to the queue, lower-priority tasks may never be executed, causing hunger.

Tasks with different execution times can be assigned to thread pools of different sizes, or the priority PriorityBlockingQueue can be used to allow tasks with shorter execution times to execute first.

For example, a task that relies on a database connection pool, because the longer a thread waits for the database to return a result after submitting SQL, the longer the CPU will be idle, so the higher the number of threads should be set to make better use of the CPU.

Use bounded queues. A bounded queue can increase the stability and early warning of the system and can be set as large as necessary, such as several thousand. If the queue is set to unbounded, the thread pool will have more and more queues, which may fill up the memory and raise the OOM, causing the entire system to become unavailable.

Monitor thread pools

If thread pools are heavily used in a system, it is necessary to monitor the pool so that problems can be quickly identified if they occur. This can be monitored using the thread pool parameters:

  • TaskCount: indicates the number of tasks to be executed by the thread pool.

  • CompletedTaskCount: The number of tasks that have been completed during the run of the thread pool, less than or equal to taskCount.

  • LargestPoolSize: Maximum number of threads that have ever been created in the thread pool. This data lets you know if the thread pool has ever been full. If this value is equal to maximumPoolSize, the thread pool has been full.

  • GetPoolSize: Indicates the number of threads in the thread pool. Threads in the pool do not self-destruct if the pool is not destroyed, so the size only increases.

  • GetActiveCount: Gets the number of active threads.

Monitor by extending the thread pool. You can define a thread pool by inheriting the thread pool, override the thread pool’s beforeExecute, afterExecute, and terminated methods, and execute some code to monitor tasks before, after, and before the thread pool is closed. For example, the average execution time, maximum execution time, and minimum execution time of monitoring tasks. These methods are empty methods in the thread pool.

Methods in ThreadPoolExecutor, which are still empty and can be extended.

protected void beforeExecute(Thread t, Runnable r) {}protected void afterExecute(Runnable r, Throwable t) {}protected void terminated(a) {}Copy the code