ScheduledExecutorService使用介绍

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JUC包(java.util.concurrent)中提供了对定时任务的支持,即ScheduledExecutorService接口。本文对ScheduledExecutorService的介绍,将基于Timer类使用介绍进行,因此请先阅读Timer类使用介绍文章。

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

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

1. Timer使用介绍

一、创建ScheduledExecutorService对象

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

二、ScheduledExecutorService方法

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

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

  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):延迟delay单位时间后,执行一次任务
  • ScheduledFuture schedule(Callable 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内部是一个线程池,当抛出异常时,此任务所在线程将会终止运行被回收,该任务后续无法再触发执行,其他线程不受影响,因此编写任务执行代码要注意捕获异常。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
存储 Linux 调度
APScheduler
【10月更文挑战第09天】
24 2
|
6月前
|
Java 调度
ScheduledExecutorService与ExecutorService区别
ScheduledExecutorService与ExecutorService区别
|
5月前
|
Java 调度
使用ScheduledThreadPoolExecutor进行任务调度
使用ScheduledThreadPoolExecutor进行任务调度
|
5月前
|
缓存 安全 Java
使用ScheduledExecutorService进行任务调度
使用ScheduledExecutorService进行任务调度
|
7月前
|
调度
APScheduler任务相关操作
APScheduler任务相关操作
50 0
|
7月前
|
资源调度 分布式计算 算法
Gang Scheduling
Gang Scheduling(Coscheduling)、FIFO Scheduling、Capacity Scheduling、Fair sharing、Binpack/Spread等是云计算和分布式系统中的任务调度算法,用于在资源有限的情况下,公平、高效地分配任务和资源。下面是这些调度算法的基本介绍和如何在实际应用中使用它们的一些建议:
302 2
|
Java 调度
ScheduledExecutorService:多线程任务调度
ScheduledExecutorService:多线程任务调度
784 0
ScheduledExecutorService:多线程任务调度
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
50 0
|
Java 索引
并发编程——ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是ThreadPoolExecutor的一个子类,在线程池的基础上实现了延迟执行任务以及周期性执行任务的功能。
60 0