Dive Deep Into ScheduledExecutorService’s Internals

java data structures

7 min read

Java's ScheduledExecutorService is the ultimate go-to solution when it comes to running task threads periodically or with a fixed (including a delay of 0) interval.

But, have you ever wondered how this all works internally? Like how are different race conditions solved? Or how are tasks delayed for some time and picked up right when they are required?

In this article, we delve into the internal mechanisms of this exceptional library. We explore the underlying data structures that support its functionality, uncover the strategies employed to handle race conditions, and examine the seamless coordination among multiple internal classes that ensures smooth operation.

This will be really interesting so, without further ado, let's begin.


Tell me the basics first

In this section, we'll take a brief look at the essential classes that contribute to understanding the ScheduledExecutorService and how they are interconnected.

The diagram given below shows three significant components: Executor, ThreadPoolExecutor, and ScheduledThreadPoolExecutor. Now, let's try to get a gist of these components.

Alt text

Executors

This class contains all the factory and utility methods that are required to create or manage thread pools. You would have used it at some point to create one yourself. An example code snippet of how it is used is given below:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

This code snippet will internally create a ScheduledThreadPoolExecutor of 10 threads. Also, the code given above creates a ScheduledThreadPoolExecutor, but it can be used to create a variety of different thread pools.

ThreadPoolExecutor

At the core of this library, the ThreadPoolExecutor class performs various vital functions, including configuring and managing the thread pool, handling task execution, and maintaining necessary bookkeeping.

If you closely observed the diagram mentioned earlier, you would have noticed two significant sub-components within the ThreadPoolExecutor.

Worker

The first one is the Worker, a nested class defined within the ThreadPoolExecutor. When we refer to a thread pool, we implicitly refer to these workers. It is the responsibility of the ThreadPoolExecutor to manage this worker pool efficiently. This includes adding workers, running workers, interrupting workers, etc.

Whenever a client submits a task to the ExecutorService, these are picked up by one of the worker threads and executed.

BlockingQueue

The second crucial attribute is BlockingQueue. By default, the work queue employed within the ThreadPoolExecutor is a BlockingQueue. Its primary function is to receive tasks from the client thread and distribute them among the worker threads in a thread-safe manner.

ScheduledThreadPoolExecutor

The ScheduledThreadPoolExecutor class is an extension of ThreadPoolExecutor. It leverages various methods from its parent class while introducing a single modification: the work queue definition. Instead of a simple BlockingQueue, it utilizes an extended version known as DelayedWorkQueue.

This specialized data structure lies at the heart of the scheduling mechanism. We will go through it in detail in the next section.

Having understood the various classes involved, let's dive deeper into their intricate details.


Let's focus on the core

When a task is submitted to the ScheduledExecutorService, it gets added to the DelayedWorkQueue.

This queue is continuously polled by the available worker threads. Once a task is detected in the queue, the worker threads contend for a lock to remove the task. This locking mechanism ensures that multiple workers do not attempt to process the same task simultaneously.

Now, here's the intriguing part: the work queue utilized by ScheduledExecutorService is not a typical queue; it's a min-heap. Tasks within this heap are sorted based on their scheduled execution delay. This means a task scheduled at time T0 will be positioned above a task scheduled at time T1, given that T1 > T0.

So, when the worker threads poll the queue, they also check whether the task at the head of the queue is ready for execution or not based on the current timestamp and the timestamp at which the task was supposed to run.

Once a task becomes eligible, it is extracted from the queue and executed promptly. An example code snippet to schedule a task with a fixed delay of 5 seconds is given below:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
}, 5, TimeUnit.SECONDS);

The explanation provided above effectively covers the scenario of a one-time scheduled task. Now, let's consider recurring tasks and instant tasks, which are handled similarly.

Recurring tasks

After a recurring task completes its execution for the first time, it is reintroduced into the work queue with the given delay. This cycle continues until the task is explicitly canceled by the client.

An example code snippet to run a recurring task with an initial delay of 0 seconds and then a periodic interval of 5 seconds is given below:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
}, 0, 5, TimeUnit.SECONDS);

Instant tasks

Instant tasks are a subset of tasks with a fixed delay. In this case, however, the fixed delay is set to 0. Thus, instant tasks are executed immediately upon being scheduled. An example code snippet to run the task instantly is given below:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
});

Ending notes

In this article, we have explored the internal mechanisms of the ScheduledExecutorService without delving into unnecessary complexity. The elegance of its implementation within Java is truly remarkable. By comprehending its internals, you can enhance your skills as a developer, as the principles learned can be applied to various scenarios that involve the utilization of a delay queue.

Hope you learned something new today.

Thank you for reading. Happy coding!