文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文搞懂Java ScheduledExecutorService的使用

2022-11-13 19:13

关注

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

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

此处为语雀内容卡片,点击链接查看

一、创建ScheduledExecutorService对象

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

二、ScheduledExecutorService方法

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

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

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

以上就是一文搞懂Java ScheduledExecutorService的使用的详细内容,更多关于Java ScheduledExecutorService的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯