ScheduledExecutorService使用介绍

JUC包(java.util.concurrent)中提供了对定时任务的支持,即ScheduledExecutorService接口。

本文对ScheduledExecutorService的介绍,将基于Timer类使用介绍进行,因此请先阅读Timer类使用介绍文章。

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/unicorntopcode/java-scheduler/okn77s

一、创建ScheduledExecutorService对象

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

二、ScheduledExecutorService方法

ScheduledExecutorService实现了ExecutorService接口,ExecutorService接口中的方法事实上属于线程池相关的一般方法,不在本文讨论。

ScheduledExecutorService本身提供了以下4个方法:

  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):延迟delay单位时间后,执行一次任务

  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):延迟delay单位时间后,执行一次任务

  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟initialDelay单位时间后,执行一次任务,之后每隔period单位时间执行一次任务(固定速率)

  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):延迟initialDelay单位时间后,执行一次任务,之后每隔period单位时间执行一次任务(固定延时)

ScheduledExecutorService和Timer进行对比,两者所提供的方法是类似的,区别在于Timer有提供指定时间点执行任务,而ScheduledExecutorService没有提供。

Timer提供的方法返回值均为void,而ScheduledExecutorService的方法返回值均为ScheduledFuture(继承于Future接口)。

三、固定速率和固定延时的区别

和Timer一样,我们用示例来展示ScheduledExecutorService固定速率和固定延时的区别,并与Timer进行对比。

1. 固定速率

示例:

System.out.println("启动于:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleAtFixedRate(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " 开始执行, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " 结束");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

输出:

启动于:2022-10-31 17:15:44
1 2022-10-31 17:15:49 开始执行, 2022-10-31 17:15:49 结束
2 2022-10-31 17:15:51 开始执行, 2022-10-31 17:15:51 结束
3 2022-10-31 17:15:53 开始执行, 2022-10-31 17:16:04 结束 *
4 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
5 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
6 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
7 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
8 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
9 2022-10-31 17:16:05 开始执行, 2022-10-31 17:16:05 结束
10 2022-10-31 17:16:07 开始执行, 2022-10-31 17:16:07 结束
11 2022-10-31 17:16:09 开始执行, 2022-10-31 17:16:09 结束

没有11秒耗时的情况下,正常应该是输出:

启动于:2022-10-31 17:15:44
1 2022-10-31 17:15:49 开始执行, 2022-10-31 17:15:49 结束
2 2022-10-31 17:15:51 开始执行, 2022-10-31 17:15:51 结束
3 2022-10-31 17:15:53 开始执行, 2022-10-31 17:15:53 结束
4 2022-10-31 17:15:55 开始执行, 2022-10-31 17:15:55 结束
5 2022-10-31 17:15:57 开始执行, 2022-10-31 17:15:57 结束
6 2022-10-31 17:15:59 开始执行, 2022-10-31 17:15:59 结束
7 2022-10-31 17:16:01 开始执行, 2022-10-31 17:16:01 结束
8 2022-10-31 17:16:03 开始执行, 2022-10-31 17:16:03 结束
9 2022-10-31 17:16:05 开始执行, 2022-10-31 17:16:05 结束
10 2022-10-31 17:16:07 开始执行, 2022-10-31 17:16:07 结束
11 2022-10-31 17:16:09 开始执行, 2022-10-31 17:16:09 结束

从测试结果中可以看出,当有一次任务执行耗时过长,超出了设定的period时间单位,将会影响后续5次任务准时执行,当耗时任务完成后,ScheduledExecutorService将会立即将延误的5次任务一起补上,并保障后续的任务按预期的时间点执行。

这与ScheduledExecutorService固定速率的效果与Timer是完全一样的,读者可直接参考Timer的固定速率介绍。

2. 固定延时

示例:

System.out.println("启动于:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleWithFixedDelay(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " 开始执行, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " 结束");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

输出:

1 2022-10-31 17:16:41 开始执行, 2022-10-31 17:16:41 结束
2 2022-10-31 17:16:43 开始执行, 2022-10-31 17:16:43 结束
3 2022-10-31 17:16:45 开始执行, 2022-10-31 17:16:56 结束 *
4 2022-10-31 17:16:58 开始执行, 2022-10-31 17:16:58 结束
5 2022-10-31 17:17:00 开始执行, 2022-10-31 17:17:00 结束
6 2022-10-31 17:17:02 开始执行, 2022-10-31 17:17:02 结束
7 2022-10-31 17:17:04 开始执行, 2022-10-31 17:17:04 结束
8 2022-10-31 17:17:06 开始执行, 2022-10-31 17:17:06 结束
9 2022-10-31 17:17:08 开始执行, 2022-10-31 17:17:08 结束

没有11秒耗时的情况下,正常应该是输出:

1 2022-10-31 17:16:41 开始执行, 2022-10-31 17:16:41 结束
2 2022-10-31 17:16:43 开始执行, 2022-10-31 17:16:43 结束
3 2022-10-31 17:16:45 开始执行, 2022-10-31 17:16:45 结束
4 2022-10-31 17:16:47 开始执行, 2022-10-31 17:16:47 结束
5 2022-10-31 17:16:49 开始执行, 2022-10-31 17:16:49 结束
6 2022-10-31 17:16:51 开始执行, 2022-10-31 17:16:51 结束
7 2022-10-31 17:16:53 开始执行, 2022-10-31 17:16:53 结束
8 2022-10-31 17:16:55 开始执行, 2022-10-31 17:16:55 结束
9 2022-10-31 17:16:57 开始执行, 2022-10-31 17:16:57 结束

固定延时是当任务执行耗时过长,超出设定的delay时间单位,后续的任务将会被顺延推迟,这个设计是与Timer一样的,但与Timer却有一点小区别。

在Timer类使用介绍中,曾提到Timer类固定延时下与我想象的不太一致,Timer在第3次任务执行完成后会立即执行第4次任务,接着才是间隔2秒执行第5次任务。

而ScheduledExecutorService则与我的想象完全一致,当第3次任务执行完成后,会间隔2秒再执行第4次任务。

所以固定延时下,Timer和ScheduledExecutorService的实现是有一点区别的。

四、调度多个任务

在Timer中,一个TimerTask对象是一个任务。

而在ScheduledExecutorService中,则一个Runnable对象一个任务。

第三节介绍的是固定速率和固定延时是如何影响一个可重复执行任务(一个Runnable对象)的多次执行的。

而本节介绍的是ScheduledExecutorService如何同时调度多个可重复执行任务的。

与Timer内部仅1个线程不同,ScheduledExecutorService内部采用的是线程池,是支持自己设定线程数的。

那么理论上来说,如果要加入2个任务,ScheduledExecutorService设定线程数为2,就不会出现相互影响的情况。

我们来验证一下。

定义任务,当执行第3次时将会休眠11秒:

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() + " 开始执行");
        if(i == 3) {
            ThreadUtil.sleep(11 * 1000);
        }
        System.out.println(i + " " + name + ":" + DateUtil.formatNow() + " 执行结束");
        i ++;
    }
}

使用ScheduledExecutorService进行调度:

System.out.println("启动于:" + 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);

由于控制台输出时,task1和task2的日志会混在一起,不容易阅读,我这边将task1和task2的日志分开。

task1日志:

启动于:2022-10-31 17:49:51
1 task1:2022-10-31 17:49:56 开始执行
1 task1:2022-10-31 17:49:56 执行结束
2 task1:2022-10-31 17:49:58 开始执行
2 task1:2022-10-31 17:49:58 执行结束
3 task1:2022-10-31 17:50:00 开始执行
3 task1:2022-10-31 17:50:11 执行结束
4 task1:2022-10-31 17:50:13 开始执行
4 task1:2022-10-31 17:50:13 执行结束
5 task1:2022-10-31 17:50:15 开始执行
5 task1:2022-10-31 17:50:15 执行结束

task2日志:

启动于:2022-10-31 17:49:51
1 task2:2022-10-31 17:49:56 开始执行
1 task2:2022-10-31 17:49:56 执行结束
2 task2:2022-10-31 17:49:58 开始执行
2 task2:2022-10-31 17:49:58 执行结束
3 task2:2022-10-31 17:50:00 开始执行
3 task2:2022-10-31 17:50:11 执行结束
4 task2:2022-10-31 17:50:13 开始执行
4 task2:2022-10-31 17:50:13 执行结束
5 task2:2022-10-31 17:50:15 开始执行

经过测试可以确定,当加入的任务数不超过线程池线程数时,即使任务存在耗时也不会相互影响,而仅是影响自身任务下一次执行的时间点。

那如果加入任务数超出了线程数呢?

我们测试一下加入3个任务,线程数仍然为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);

将三个任务的日志分开展示。

task1:

启动于:2022-10-31 17:53:22
1 task1:2022-10-31 17:53:27 开始执行
1 task1:2022-10-31 17:53:27 执行结束
2 task1:2022-10-31 17:53:29 开始执行
2 task1:2022-10-31 17:53:29 执行结束
3 task1:2022-10-31 17:53:31 开始执行
3 task1:2022-10-31 17:53:42 执行结束
4 task1:2022-10-31 17:53:44 开始执行
4 task1:2022-10-31 17:53:44 执行结束
5 task1:2022-10-31 17:53:46 开始执行
5 task1:2022-10-31 17:53:46 执行结束
6 task1:2022-10-31 17:53:48 开始执行
6 task1:2022-10-31 17:53:48 执行结束
7 task1:2022-10-31 17:53:50 开始执行
7 task1:2022-10-31 17:53:50 执行结束
8 task1:2022-10-31 17:53:52 开始执行
8 task1:2022-10-31 17:53:52 执行结束
9 task1:2022-10-31 17:53:54 开始执行
9 task1:2022-10-31 17:53:54 执行结束
10 task1:2022-10-31 17:53:56 开始执行
10 task1:2022-10-31 17:53:56 执行结束

task2:

启动于:2022-10-31 17:53:22
1 task2:2022-10-31 17:53:27 开始执行
1 task2:2022-10-31 17:53:27 执行结束
2 task2:2022-10-31 17:53:29 开始执行
2 task2:2022-10-31 17:53:29 执行结束
3 task2:2022-10-31 17:53:31 开始执行
3 task2:2022-10-31 17:53:42 执行结束
4 task2:2022-10-31 17:53:44 开始执行
4 task2:2022-10-31 17:53:44 执行结束
5 task2:2022-10-31 17:53:46 开始执行
5 task2:2022-10-31 17:53:46 执行结束
6 task2:2022-10-31 17:53:48 开始执行
6 task2:2022-10-31 17:53:48 执行结束
7 task2:2022-10-31 17:53:50 开始执行
7 task2:2022-10-31 17:53:50 执行结束
8 task2:2022-10-31 17:53:52 开始执行
8 task2:2022-10-31 17:53:52 执行结束
9 task2:2022-10-31 17:53:54 开始执行
9 task2:2022-10-31 17:53:54 执行结束
10 task2:2022-10-31 17:53:56 开始执行
10 task2:2022-10-31 17:53:56 执行结束

task3:

启动于:2022-10-31 17:53:22
1 task3:2022-10-31 17:53:27 开始执行
1 task3:2022-10-31 17:53:27 执行结束
2 task3:2022-10-31 17:53:29 开始执行
2 task3:2022-10-31 17:53:29 执行结束
3 task3:2022-10-31 17:53:42 开始执行
3 task3:2022-10-31 17:53:53 执行结束
4 task3:2022-10-31 17:53:55 开始执行
4 task3:2022-10-31 17:53:55 执行结束
5 task3:2022-10-31 17:53:57 开始执行
5 task3:2022-10-31 17:53:57 执行结束

从以上日志可以看出,task1和task2执行是正常的,但是task3从第3次执行开始出现错误。

task3第三次时间点正确时间应该是17:53:31,而实际上被推迟到了17:53:42才开始。

从这点我们可以推测出,当时2个线程都在执行task1、task2的耗时11秒的第3次任务,导致task3被推迟。

因此,我们在使用ScheduledExecutorService调度多个任务时,应注意尽可能缩短任务的处理耗时,以及避免任务数超出线程数。

五、其他要点

任务执行过程中抛出异常会发生什么情况?

Timer内部是单个线程处理所有任务,当抛出异常时,Timer线程将终止运行;

ScheduledExecutorService内部是一个线程池,当抛出异常时,此任务所在线程将会终止运行被回收,该任务后续无法再触发执行,其他线程不受影响,因此编写任务执行代码要注意捕获异常。

posted @ 2022-11-01 21:09  UnicornLien  阅读(335)  评论(0编辑  收藏  举报