Trigger ThreadPoolExecutor through custom blocking queue to create non core threads to execute tasks in unbounded queue

Posted by sgoku01 on Sun, 07 Nov 2021 02:28:19 +0100

Java projects often use ThreadPoolExecutor to create thread pools. The core parameters include corePoolSize, maximumPoolSize and workQueue. We hope that the constructed thread pool can meet the following conditions:

  1. The number of threads is controllable. You need to set a maximum number of threads, maximumPoolSize, to prevent unlimited creation of threads and exhaustion of system resources.
  2. The tasks placed in the thread pool will not be rejected or discarded (if the task is discarded, it will lead to serious business bugs). Therefore, an unbounded blocking queue is generally defined (no size is specified, and the maximum capacity is Integer.MAX_VALUE) to cache tasks to be executed.

Unbounded queue results in invalid maximumPoolSize

The logic of the execute() method of ThreadPoolExecutor is as follows: to add a new task, first judge whether the core thread is free, and if the core thread is free, hand it over to the core thread for execution; If there is no idle core thread, the task is put into the blocking queue.

1. Three steps of task implementation

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
		
		// 1. If there is an idle core thread, it shall be executed by the core thread
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
		
		// 2. Add the task to the queue and wait for the idle thread to consume
        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);
        }
        
        // 3. Try to create a non core thread to execute the task
        else if (!addWorker(command, false))
            reject(command);
    }

2. The unbounded queue workQueue.offer(command) always returns true

Take LinkedBlockingDeque to see the implementation process of its offer:

  1. ThreadPoolExecutor.execute() calls LinkedBlockingDeque.offer().
  2. LinkedBlockingDeque.offer() calls LinkedBlockingDeque.offerLast().
  3. LinkedBlockingDeque.offerLast() calls LinkedBlockingDeque.linkLast().
    private boolean linkLast(Node<E> node) {
        // Adding failed when the queue capacity was exceeded. Returns false.
        if (count >= capacity)
            return false;
        Node<E> l = last;
        node.prev = l;
        last = node;
        if (first == null)
            first = node;
        else
            l.next = node;
        ++count;
        notEmpty.signal();
        return true;
    }

For unbounded queues, count > = integer.max is required_ Value will return false, triggering the creation of a non core thread. This condition is basically impossible to achieve, so the thread pool shows that only the core thread is working.

The method of overriding LinkedBlockingDeque by custom blocking queue triggers the creation of non core threads

Consideration: from the perspective of the call chain, we can rewrite the linkLast method and modify the if (count > = capacity) judgment condition. However, linkLast() is a private method, and subclasses cannot be overridden; The upper layer offerLast() is a public method, but it depends on the internal class Node. Looking at offer(), offer() directly calls the offerLast() method, does not rely on the private model defined inside LinkedBlockingDequene, and meets the rewriting conditions.

The customized ThreadTaskLinkedBlockingQueue is used as the private internal class definition of the thread pool tool to hide the implementation details.

package elon.threadpool.util;

import lombok.Setter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * Thread pool utility class.
 *
 * @author elon
 * @since 2021/11/6
 */
public class ElonThreadPoolUtils {
    private static final Logger LOGGER = LogManager.getLogger(ElonThreadPoolUtils.class);

    private static int corePoolSize = 10;

    private static int maximumPoolSize = 100;

    private static ThreadPoolExecutor poolExecutor = null;

    private static ThreadTaskLinkedBlockingQueue<Runnable> queue = new ThreadTaskLinkedBlockingQueue<>();

    public static void initThreadPool(int corePoolSize, int maximumPoolSize){
        ElonThreadPoolUtils.corePoolSize = corePoolSize;
        ElonThreadPoolUtils.maximumPoolSize = maximumPoolSize;

        poolExecutor = new ThreadPoolExecutor(ElonThreadPoolUtils.corePoolSize, ElonThreadPoolUtils.maximumPoolSize, 10,
                TimeUnit.SECONDS, queue);

        LOGGER.info("[ElonThreadPoolUtils]Init thread pool success. corePoolSize:{}|maximumPoolSize:{}", corePoolSize,
                maximumPoolSize);
    }

    synchronized public static void executeTask(Runnable task){
        int activeThreadNum = poolExecutor.getActiveCount();
        LOGGER.info("[ElonThreadPoolUtils]Number of active threads:{}", activeThreadNum);
        LOGGER.info("[ElonThreadPoolUtils]The number of tasks waiting to be processed in the queue:{}", queue.size());

        poolExecutor.execute(task);
    }

    /**
     * Custom thread task blocking queue. When the number of active threads is less than the maximum number of supported threads, new tasks are not placed in the queue, so as to stimulate the thread pool to create new threads for timely processing
     * Solve the problem of using LinkedBlockingDeque infinite queue. Only core threads are processing in the thread pool. maximumPoolSize is not enabled.
     *
     * @author elon
     * @since 2021/11/6
     */
    @Setter
    private static class ThreadTaskLinkedBlockingQueue<E> extends LinkedBlockingDeque<E> {
        @Override
        public boolean offer(E e) {
            int activeThreadNum = poolExecutor.getActiveCount();
            if (activeThreadNum < maximumPoolSize) {
                return false;
            }

            return offerLast(e);
        }
    }
}

The key logic is to rewrite the implementation of offer() and add judgment before calling offerLast(): if the number of active threads in the thread pool is less than the maximum number of threads, the new task directly returns false; Not put in the queue. This triggers the thread pool to create non core threads to execute tasks.

If the number of active threads is equal to the maximum number of threads, the task will be placed in the queue waiting for idle threads to consume.

Source address: https://github.com/ylforever/elon-threadpool

Topics: Java thread pool