Introduction to the use of ScheduledExecutorService

The JUC package (java.util.concurrent) provides support for scheduled tasks, namely the ScheduledExecutorService interface.

The introduction of ScheduledExecutorService in this article will be based on the introduction of Timer class usage, so please read the Timer class usage introduction article first.

Here is the Yuque content card, click the link to view: https://www.yuque.com/unicorntopcode/java-scheduler/okn77s

1. Create a ScheduledExecutorService object

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

2. ScheduledExecutorService method

ScheduledExecutorService implements the ExecutorService interface. The methods in the ExecutorService interface are actually general methods related to thread pools and are not discussed in this article.

ScheduledExecutorService itself provides the following 4 methods:

  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit): After delaying the delay unit time, execute a task

  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit): After delaying the delay unit time, execute a task

  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): After delaying the initialDelay unit time, execute the task once, and then execute the task every period unit time (fixed rate)

  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): After delaying the initialDelay unit time, execute the task once, and then execute the task every period unit time (fixed delay)

Compared ScheduledExecutorService and Timer, the methods provided by the two are similar, the difference is that Timer provides tasks to execute at specified time points, while ScheduledExecutorService does not.

The return values โ€‹โ€‹of the methods provided by Timer are all void, and the return values โ€‹โ€‹of the methods of ScheduledExecutorService are ScheduledFuture (inherited from the Future interface).

Third, the difference between fixed rate and fixed delay

Like Timer, we use an example to show the difference between ScheduledExecutorService fixed rate and fixed delay, and compare it with Timer.

1. Fixed rate

Example:

System.out.println("Started at:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleAtFixedRate(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " start execution, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " Finish");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

output:

Start: 2022-10-31 17:15:44
1 2022-10-31 17:15:49 start execution, 2022-10-31 17:15:49 Finish
2 2022-10-31 17:15:51 start execution, 2022-10-31 17:15:51 Finish
3 2022-10-31 17:15:53 start execution, 2022-10-31 17:16:04 Finish *
4 2022-10-31 17:16:04 start execution, 2022-10-31 17:16:04 Finish *
5 2022-10-31 17:16:04 start execution, 2022-10-31 17:16:04 Finish *
6 2022-10-31 17:16:04 start execution, 2022-10-31 17:16:04 Finish *
7 2022-10-31 17:16:04 start execution, 2022-10-31 17:16:04 Finish *
8 2022-10-31 17:16:04 start execution, 2022-10-31 17:16:04 Finish *
9 2022-10-31 17:16:05 start execution, 2022-10-31 17:16:05 Finish
10 2022-10-31 17:16:07 start execution, 2022-10-31 17:16:07 Finish
11 2022-10-31 17:16:09 start execution, 2022-10-31 17:16:09 Finish

In the absence of 11 seconds, the normal output should be:

Start: 2022-10-31 17:15:44
1 2022-10-31 17:15:49 start execution, 2022-10-31 17:15:49 Finish
2 2022-10-31 17:15:51 start execution, 2022-10-31 17:15:51 Finish
3 2022-10-31 17:15:53 start execution, 2022-10-31 17:15:53 Finish
4 2022-10-31 17:15:55 start execution, 2022-10-31 17:15:55 Finish
5 2022-10-31 17:15:57 start execution, 2022-10-31 17:15:57 Finish
6 2022-10-31 17:15:59 start execution, 2022-10-31 17:15:59 Finish
7 2022-10-31 17:16:01 start execution, 2022-10-31 17:16:01 Finish
8 2022-10-31 17:16:03 start execution, 2022-10-31 17:16:03 Finish
9 2022-10-31 17:16:05 start execution, 2022-10-31 17:16:05 Finish
10 2022-10-31 17:16:07 start execution, 2022-10-31 17:16:07 Finish
11 2022-10-31 17:16:09 start execution, 2022-10-31 17:16:09 Finish

It can be seen from the test results that when a task execution takes too long and exceeds the set period time unit, it will affect the timely execution of the subsequent 5 tasks. When the time-consuming task is completed, ScheduledExecutorService will immediately delay. Make up for the 5 tasks of the previous one together, and ensure that the subsequent tasks are executed at the expected time point.

This is exactly the same as the fixed rate effect of ScheduledExecutorService and Timer. Readers can directly refer to Timer's fixed rate introduction.

2. Fixed delay

Example:

System.out.println("Started at:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleWithFixedDelay(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " start execution, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " Finish");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

output:

1 2022-10-31 17:16:41 start execution, 2022-10-31 17:16:41 Finish
2 2022-10-31 17:16:43 start execution, 2022-10-31 17:16:43 Finish
3 2022-10-31 17:16:45 start execution, 2022-10-31 17:16:56 Finish *
4 2022-10-31 17:16:58 start execution, 2022-10-31 17:16:58 Finish
5 2022-10-31 17:17:00 start execution, 2022-10-31 17:17:00 Finish
6 2022-10-31 17:17:02 start execution, 2022-10-31 17:17:02 Finish
7 2022-10-31 17:17:04 start execution, 2022-10-31 17:17:04 Finish
8 2022-10-31 17:17:06 start execution, 2022-10-31 17:17:06 Finish
9 2022-10-31 17:17:08 start execution, 2022-10-31 17:17:08 Finish

In the absence of 11 seconds, the normal output should be:

1 2022-10-31 17:16:41 start execution, 2022-10-31 17:16:41 Finish
2 2022-10-31 17:16:43 start execution, 2022-10-31 17:16:43 Finish
3 2022-10-31 17:16:45 start execution, 2022-10-31 17:16:45 Finish
4 2022-10-31 17:16:47 start execution, 2022-10-31 17:16:47 Finish
5 2022-10-31 17:16:49 start execution, 2022-10-31 17:16:49 Finish
6 2022-10-31 17:16:51 start execution, 2022-10-31 17:16:51 Finish
7 2022-10-31 17:16:53 start execution, 2022-10-31 17:16:53 Finish
8 2022-10-31 17:16:55 start execution, 2022-10-31 17:16:55 Finish
9 2022-10-31 17:16:57 start execution, 2022-10-31 17:16:57 Finish

The fixed delay is that when the task execution takes too long and exceeds the set delay time unit, the subsequent tasks will be postponed. This design is the same as the Timer, but it is slightly different from the Timer.

In the introduction to the use of the Timer class, it was mentioned that the fixed delay of the Timer class is not consistent with what I imagined. The Timer will execute the fourth task immediately after the third task execution is completed, and then execute the fifth task at an interval of 2 seconds. secondary task.

The ScheduledExecutorService is exactly what I imagined. When the third task is executed, the fourth task will be executed at an interval of 2 seconds.

Therefore, under the fixed delay, the implementation of Timer and ScheduledExecutorService is a little different.

4. Scheduling multiple tasks

In Timer, a TimerTask object is a task.

In ScheduledExecutorService, a Runnable object is a task.

Section 3 describes how fixed rate and fixed delay affect multiple executions of a repeatable task (a Runnable object).

This section describes how ScheduledExecutorService schedules multiple repeatable tasks at the same time.

Unlike Timer, which has only one thread, ScheduledExecutorService uses a thread pool, which supports setting the number of threads by itself.

Then in theory, if you want to add 2 tasks, ScheduledExecutorService sets the number of threads to 2, and there will be no mutual influence.

Let's verify.

Define a task that will sleep for 11 seconds when executed for the third time:

class Task implements Runnable {

    private int i = 1;

    private String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(i + " " + name + ":" + DateUtil.formatNow() + " start execution");
        if(i == 3) {
            ThreadUtil.sleep(11 * 1000);
        }
        System.out.println(i + " " + name + ":" + DateUtil.formatNow() + " execution ends");
        i ++;
    }
}

Use ScheduledExecutorService for scheduling:

System.out.println("Started at:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

Task task1 = new Task("task1");
Task task2 = new Task("task2");

executorService.scheduleWithFixedDelay(task1, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task2, 5, 2, TimeUnit.SECONDS);

Since the logs of task1 and task2 will be mixed together when the console is output, it is not easy to read. I will separate the logs of task1 and task2 here.

task1 log:

Start: 2022-10-31 17:49:51
1 task1:2022-10-31 17:49:56 start execution
1 task1:2022-10-31 17:49:56 execution ends
2 task1:2022-10-31 17:49:58 start execution
2 task1:2022-10-31 17:49:58 execution ends
3 task1:2022-10-31 17:50:00 start execution
3 task1:2022-10-31 17:50:11 execution ends
4 task1:2022-10-31 17:50:13 start execution
4 task1:2022-10-31 17:50:13 execution ends
5 task1:2022-10-31 17:50:15 start execution
5 task1:2022-10-31 17:50:15 execution ends

task2 log:

Start: 2022-10-31 17:49:51
1 task2:2022-10-31 17:49:56 start execution
1 task2:2022-10-31 17:49:56 execution ends
2 task2:2022-10-31 17:49:58 start execution
2 task2:2022-10-31 17:49:58 execution ends
3 task2:2022-10-31 17:50:00 start execution
3 task2:2022-10-31 17:50:11 execution ends
4 task2:2022-10-31 17:50:13 start execution
4 task2:2022-10-31 17:50:13 execution ends
5 task2:2022-10-31 17:50:15 start execution

After testing, it can be determined that when the number of added tasks does not exceed the number of threads in the thread pool, even if the tasks are time-consuming, they will not affect each other, but only affect the time point when the next execution of their own tasks is performed.

What if the number of added tasks exceeds the number of threads?

Let's test adding 3 tasks, and the number of threads is still 2.

Task task1 = new Task("task1");
Task task2 = new Task("task2");
Task task3 = new Task("task3");

executorService.scheduleWithFixedDelay(task1, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task2, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task3, 5, 2, TimeUnit.SECONDS);

Display the logs of the three tasks separately.

task1:

Start: 2022-10-31 17:53:22
1 task1:2022-10-31 17:53:27 start execution
1 task1:2022-10-31 17:53:27 execution ends
2 task1:2022-10-31 17:53:29 start execution
2 task1:2022-10-31 17:53:29 execution ends
3 task1:2022-10-31 17:53:31 start execution
3 task1:2022-10-31 17:53:42 execution ends
4 task1:2022-10-31 17:53:44 start execution
4 task1:2022-10-31 17:53:44 execution ends
5 task1:2022-10-31 17:53:46 start execution
5 task1:2022-10-31 17:53:46 execution ends
6 task1:2022-10-31 17:53:48 start execution
6 task1:2022-10-31 17:53:48 execution ends
7 task1:2022-10-31 17:53:50 start execution
7 task1:2022-10-31 17:53:50 execution ends
8 task1:2022-10-31 17:53:52 start execution
8 task1:2022-10-31 17:53:52 execution ends
9 task1:2022-10-31 17:53:54 start execution
9 task1:2022-10-31 17:53:54 execution ends
10 task1:2022-10-31 17:53:56 start execution
10 task1:2022-10-31 17:53:56 execution ends

task2:

Start: 2022-10-31 17:53:22
1 task2:2022-10-31 17:53:27 start execution
1 task2:2022-10-31 17:53:27 execution ends
2 task2:2022-10-31 17:53:29 start execution
2 task2:2022-10-31 17:53:29 execution ends
3 task2:2022-10-31 17:53:31 start execution
3 task2:2022-10-31 17:53:42 execution ends
4 task2:2022-10-31 17:53:44 start execution
4 task2:2022-10-31 17:53:44 execution ends
5 task2:2022-10-31 17:53:46 start execution
5 task2:2022-10-31 17:53:46 execution ends
6 task2:2022-10-31 17:53:48 start execution
6 task2:2022-10-31 17:53:48 execution ends
7 task2:2022-10-31 17:53:50 start execution
7 task2:2022-10-31 17:53:50 execution ends
8 task2:2022-10-31 17:53:52 start execution
8 task2:2022-10-31 17:53:52 execution ends
9 task2:2022-10-31 17:53:54 start execution
9 task2:2022-10-31 17:53:54 execution ends
10 task2:2022-10-31 17:53:56 start execution
10 task2:2022-10-31 17:53:56 execution ends

task3:

Start: 2022-10-31 17:53:22
1 task3:2022-10-31 17:53:27 start execution
1 task3:2022-10-31 17:53:27 execution ends
2 task3:2022-10-31 17:53:29 start execution
2 task3:2022-10-31 17:53:29 execution ends
3 task3:2022-10-31 17:53:42 start execution
3 task3:2022-10-31 17:53:53 execution ends
4 task3:2022-10-31 17:53:55 start execution
4 task3:2022-10-31 17:53:55 execution ends
5 task3:2022-10-31 17:53:57 start execution
5 task3:2022-10-31 17:53:57 execution ends

As can be seen from the above logs, task1 and task2 are executed normally, but task3 has an error since the third execution.

The correct time for the third time point of task3 should be 17:53:31, but it was actually delayed until 17:53:42.

From this point, we can infer that both threads were executing the third task of task1 and task2, which took 11 seconds, causing task3 to be delayed.

Therefore, when we use ScheduledExecutorService to schedule multiple tasks, we should pay attention to shorten the processing time of the tasks as much as possible, and avoid the number of tasks exceeding the number of threads.

5. Other points

What happens when an exception is thrown during task execution?

Inside the Timer is a single thread that handles all tasks. When an exception is thrown, the Timer thread will terminate;

There is a thread pool inside ScheduledExecutorService. When an exception is thrown, the thread where the task is located will be terminated and recycled. The task cannot be triggered for execution in the future, and other threads are not affected. Therefore, when writing task execution code, attention should be paid to catching exceptions.

Posted by PhantomCube on Tue, 01 Nov 2022 16:48:45 +0300