Exploring JUC Scheduled, Tomcat, and Fork/Join Thread Pools

1. Scheduled Task Execution with JUC Thread Pools

JUC's ScheduledExecutorService enables tasks to be executed at specific future times or with recurring delays. A common use case involves scheduling a task to run at a fixed point in time, such as every Thursday at 6:00 PM.

To achieve this, first determine the initial delay until the next target execusion time. If the current time has already passed the target time for the current week, schedule it for the following week. Subsequently, the task can repeat with a fixed delay representing one week.

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class WeeklyTaskScheduler {

    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        LocalDateTime now = LocalDateTime.now();
        System.out.println("Current time: " + now);

        // Determine the next Thursday at 18:00:00
        LocalDateTime targetTime = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);

        // If the current time is past this week's Thursday, move to next week
        if (now.compareTo(targetTime) > 0) {
            targetTime = targetTime.plusWeeks(1);
        }

        long initialDelayMillis = Duration.between(now, targetTime).toMillis();
        System.out.println("Next scheduled run: " + targetTime);

        // Define the weekly period
        long weeklyPeriodMillis = TimeUnit.DAYS.toMillis(7);

        scheduler.scheduleWithFixedDelay(() -> {
            System.out.println("Executing weekly task at: " + LocalDateTime.now());
        }, initialDelayMillis, weeklyPeriodMillis, TimeUnit.MILLISECONDS);
    }
}

2. Tomcat's Internal Thread Pool

Tomcat's architecture involves several components working together to handle incoming requests. A Server contains one or more Service instances, each comprising a Container (for managing servlets) and multiple Connectors (e.g., for HTTP and HTTPS). The Connector is crucial as it acts as the intefrace between the outside world and Tomcat, handling network communication (TCP/IP) and protocol parsing (HTTP).

2.1 Connector's NIO Endpoint

The Connector's NIO (Non-blocking I/O) Endpoint is responsible for efficient request handling. It typically includes:

  • LimitLatch: A mechanism to control the maximum number of concurrent connections, similar to J.U.C.'s Semaphore.
  • Acceptor: Dedicated thread(s) solely responsible for accepting new socket connections.
  • Poller: Thread(s) that monitor connected socket channels for I/O events (e.g., data ready to be read). Upon detecting a readable event, the Poller wraps the socket into a SocketProcessor task and submits it to an Executor thread pool.
  • Executor: The primary thread pool where worker threads process the actual HTTP requests encapsulated within SocketProcessor tasks.

In essence, the Tomcat thread pool, particularly the Executor, is dedicated to processing detected I/O events and executing request logic.

2.2 Relationship with JUC ThreadPoolExecutor

Tomcat's internal thread pool (org.apache.tomcat.util.threads.ThreadPoolExecutor) extends and customizes the standard java.util.concurrent.ThreadPoolExecutor. While it inherits most of the base functionality, a key difference lies in its rejection policy.

Unlike the standard ThreadPoolExecutor, which immediately rejects a task when both the core threads are busy and the queue is full (or if maximumPoolSize is reached with a full queue), Tomcat's implementation attempts a different strategy. When maximumPoolSize is reached and a RejectedExecutionException would typically be thrown, Tomcat first tries to force the task into its work queue using a specialized TaskQueue. Only if this secondary attempt to enqueue also fails is a RejectedExecutionException finally propagated.

Consider the execute method's override:

public void execute(Runnable task, long timeout, TimeUnit unit) {
    submittedTaskCount.incrementAndGet(); // Tracks active tasks
    try {
        super.execute(task); // Attempt standard execution
    } catch (RejectedExecutionException rejectionException) {
        // If standard execution fails, try to force into queue
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue workQueue = (TaskQueue) super.getQueue();
            try {
                // Attempt to add task to queue, ignoring max pool size for now
                if (!workQueue.force(task, timeout, unit)) {
                    submittedTaskCount.decrementAndGet();
                    throw new RejectedExecutionException("Work queue capacity is full.");
                }
            } catch (InterruptedException ie) {
                submittedTaskCount.decrementAndGet();
                Thread.interrupted();
                throw new RejectedExecutionException(ie);
            }
        } else {
            submittedTaskCount.decrementAndGet();
            throw rejectionException;
        }
    }
}

// Inside TaskQueue.java
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
    if (parent.isShutdown()) {
        throw new RejectedExecutionException("Executor is not running, cannot force command into queue");
    }
    // This directly calls ArrayBlockingQueue's offer or LinkedBlockingQueue's offer
    // bypassing ThreadPoolExecutor's check for maxPoolSize and queue capacity in some scenarios
    return super.offer(o, timeout, unit);
}

This force method essentially tries to put the task onto the queue even if ThreadPoolExecutor might otherwise create a new

Tags: JUC ThreadPoolExecutor ScheduledExecutorService Tomcat ForkJoinPool

Posted on Sat, 10 Oct 2026 16:09:41 +0000 by sdallas411