tomcat source code analysis-executor and its implementation StandardThreadExecutor

Keywords: Java Tomcat

executor

Executor is the engine, user requests are processed by executor, perception tells us this should be a multi-threaded, thread pool container, let's first look at the relationship between classes;

public interface Executor extends java.util.concurrent.Executor, Lifecycle {

    public String getName();

    @Deprecated
    void execute(Runnable command, long timeout, TimeUnit unit);
}

Executor is an interface and inherits the executor interface under jdk's juc package

StandardThreadExecutor property

standardThreadExecutor is an implementation class, as shown in the figure, which also inherits lifecycle-related interfaces and is managed by tomcat.

public class StandardThreadExecutor extends LifecycleMBeanBase
        implements Executor, ResizableExecutor {

    protected static final StringManager sm = StringManager.getManager(StandardThreadExecutor.class);
    protected int threadPriority = Thread.NORM_PRIORITY;
    //Default daemon thread, main line will exit automatically after exit, there will be no residual
    protected boolean daemon = true;

     //Prefix to Thread Name
    protected String namePrefix = "tomcat-exec-";
    //Default maximum thread 200
    protected int maxThreads = 200;
    //(int) The minimum number of threads (idle and active) is always active, defaulting to 25
    protected int minSpareThreads = 25;
    //Maximum active time 60 seconds
    protected int maxIdleTime = 60000;
     //Note that ThreadPoolExecutor here is a thread pool implemented by tomcat, not under the juc package
    protected ThreadPoolExecutor executor = null;
     //Thread Name
    protected String name;
    protected boolean prestartminSpareThreads = false;
    //Maximum Queue Size
    protected int maxQueueSize = Integer.MAX_VALUE;
//(long)If ThreadLocalLeakPreventionListener is configured, it will notify this executor about the stopped context. When the context is stopped, threads in the pool will be updated. To avoid updating all threads simultaneously, this option sets a delay between renewals of any two threads. The value is MS and the default value is 1000ms. If the value is negative, threads will not be renewed.Lifecycle Template Method
    protected long threadRenewalDelay =
        org.apache.tomcat.util.threads.Constants.DEFAULT_THREAD_RENEWAL_DELAY;
// Task Queue
private TaskQueue taskqueue = null;

Let's look at how this component starts, the startInternal() method of the lifecycle

 @Override
    protected void startInternal() throws LifecycleException {
       //Self-implemented task queue, described later
        taskqueue = new TaskQueue(maxQueueSize);
        TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
        //25 core threads, up to 200
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
        executor.setThreadRenewalDelay(threadRenewalDelay);
        if (prestartminSpareThreads) {
            executor.prestartAllCoreThreads();
        }
        taskqueue.setParent(executor);

        setState(LifecycleState.STARTING);
    }

stopInternal method

@Override
protected void stopInternal() throws LifecycleException {

    setState(LifecycleState.STOPPING);
    if (executor != null) {
        executor.shutdownNow();
    }
    executor = null;
    taskqueue = null;
}
  • Core executor method
@Override
public void execute(Runnable command, long timeout, TimeUnit unit) {
    if (executor != null) {
        executor.execute(command,timeout,unit);
    } else {
        throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted"));
    }
}

@Override
public void execute(Runnable command) {
    if (executor != null) {
        try {
            executor.execute(command);
        } catch (RejectedExecutionException rx) {
            //there could have been contention around the queue
            if (!((TaskQueue) executor.getQueue()).force(command)) {
                throw new RejectedExecutionException(sm.getString("standardThreadExecutor.queueFull"));
            }
        }
    } else {
        throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted"));
    }
}

Supplement TaskQueue

We know that the work queue is guaranteed by TaskQueue, which inherits from LinkedBlockingQueue, a blocked list queue. Look at the source code.

/**
 * As task queue specifically designed to run with a thread pool executor. The
 * task queue is optimised to properly utilize threads within a thread pool
 * executor. If you use a normal queue, the executor will spawn threads when
 * there are idle threads and you wont be able to force items onto the queue
 * itself.
 */
public class TaskQueue extends LinkedBlockingQueue<Runnable> {

    private static final long serialVersionUID = 1L;
    protected static final StringManager sm = StringManager
            .getManager("org.apache.tomcat.util.threads.res");
    private static final int DEFAULT_FORCED_REMAINING_CAPACITY = -1;

    private transient volatile ThreadPoolExecutor parent = null;

    // No need to be volatile. This is written and read in a single thread
    // (when stopping a context and firing the listeners)
    private int forcedRemainingCapacity = -1;

    public TaskQueue() {
        super();
    }

    public TaskQueue(int capacity) {
        super(capacity);
    }

    public TaskQueue(Collection<? extends Runnable> c) {
        super(c);
    }

    public void setParent(ThreadPoolExecutor tp) {
        parent = tp;
    }

    public boolean force(Runnable o) {
        if (parent == null || parent.isShutdown()) throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
        return super.offer(o); //forces the item onto the queue, to be used if the task is rejected
    }

    public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if (parent == null || parent.isShutdown()) throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
        return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected
    }

    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }


    @Override
    public Runnable poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        Runnable runnable = super.poll(timeout, unit);
        if (runnable == null && parent != null) {
            // the poll timed out, it gives an opportunity to stop the current
            // thread if needed to avoid memory leaks.
            parent.stopCurrentThreadIfNeeded();
        }
        return runnable;
    }
    @Override
    public Runnable take() throws InterruptedException {
        if (parent != null && parent.currentThreadShouldBeStopped()) {
            return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS),
                    TimeUnit.MILLISECONDS);
            // yes, this may return null (in case of timeout) which normally
            // does not occur with take()
            // but the ThreadPoolExecutor implementation allows this
        }
        return super.take();
    }
    @Override
    public int remainingCapacity() {
        if (forcedRemainingCapacity > DEFAULT_FORCED_REMAINING_CAPACITY) {
            // ThreadPoolExecutor.setCorePoolSize checks that
            // remainingCapacity==0 to allow to interrupt idle threads
            // I don't see why, but this hack allows to conform to this
            // "requirement"
            return forcedRemainingCapacity;
        }
        return super.remainingCapacity();
    }
    public void setForcedRemainingCapacity(int forcedRemainingCapacity) {
        this.forcedRemainingCapacity = forcedRemainingCapacity;
    }
    void resetForcedRemainingCapacity() {
        this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY;
    }
}

We know that TaskQueue is an infinite queue, but it overrides the offer method to return false when its thread pool size is less than maximumPoolSize, which overrides the logic of a full queue to some extent.
Fixed a bug where maxThreads failed when using the LinkedBlockingQueue default capacity of Integer.MAX_VALUE (non-core threads are opened only when the queue is full, where Integer.MAX_VALUE is never reached, so the maximum number of threads can never be reached).
This allows you to continue growing threads to maxThreads and then continue queuing after that.

TaskQueue This task queue is specifically designed for thread pools. Optimize the task queue to appropriately utilize threads within the thread pool executor.

If you use a normal queue, when an idle thread executor spawns a thread and you cannot force a task to be added to the queue.

Why not use ThreadPoolExecutor directly? Have you considered a question here?
Why does Tomcat construct a StandardThreadExecutor by itself instead of using ThreadPoolExecutor directly?
From the code above, you will find that using executor here is only using the two main methods of execute, and it wants the calling layer to block out the other methods of ThreadPoolExecutor:

  • It embodies the principle of minimum knowledge: Talk only to your close friends. That is, the client should interact with as few people as possible
  • The design pattern it represents: Facade pattern, which provides a unified interface for accessing a set of interfaces in a subsystem, making it easier for the subsystem to use

Posted by dustinkrysak on Fri, 10 Sep 2021 20:18:47 -0700