使用SpringBoot Schedule实现定时任务动态添加、修改、删除等操作
项目经常会用到定时任务,实现定时任务的方式有很多种,哪种更方便呢,我们看下:
一、Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。
Timer是调度控制器,TimerTask是可调度的任务。
1、新建可调度的任务类:MyTimerTask,继承TimerTask。
2、调动控制器调用:
然后每两秒会执行一次:
如果只执行一次,是因为没有传第二个参数,Timer 的schedule方法是有重载的
1.schedule(TimerTask task, long delay)
这个方法第二个参数是延迟,也就是延迟多少时间后执行task,而不会重复。
2. schedule(TimerTask task, long delay, long period)
这个方法有重复执行,即delay是延迟,period是周期。
缺点:使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。
二、使用Quartz
Quartz是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行。Quartz完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能,(这里不做实例,推荐springboot集成的)
持久性作业 – 就是保持调度定时的状态;
作业管理 – 对调度作业进行有效的管理;
缺点:配置起来稍显复杂
三、使用SpringBoot Schedule实现定时任务
使用相对方便,可支持动态配置,springboot支持以下几种方式:
使用@EnableScheduling注解开启对定时任务的支持。
使用@Scheduled 注解即可,基于corn、fixedRate、fixedDelay等一些定时策略来实现定时任务。
1)在spring boot的入口类Application.java中,添加@EnableScheduling允许支持schedule。
其中 @EnableScheduling 注解的作用是发现注解@Scheduled的任务并后台执行。
2)类使用@Component注解(或@Service),@Scheduled注解配置周期。
分别配置fixedDelay、fixedRate、cron
fixedDelay:控制方法执行的间隔时间,当任务执行完毕后多少时间再执行,会等待业务执行时间。
fixedRate:是按照一定的速率执行,每多少分钟执行一次,不论你业务执行花费了多少时间。
initialDelay:延迟执行时间
cron:{秒} {分} {时} {日} {月} {周} {年(可选)}
代码如下:
查看执行结果:
乍一看没啥问题,增加模拟任务执行耗时:
查看结果
并没有按预期的设置周期执行,why??!!
仔细研究之下,才知道Scheduled默认使用单线程,只有等当前任务执行完成了,后面的任务才能继续跑,这显然是有问题的,坑!
解决:增加如下配置,设置线程池默认10个。
再次启动查看结果,现在定时任务都是并行执行的,互不干扰了。说明Springboot本身默认的执行方式是串行执行,也就是说无论有多少task,都是一个线程串行执行,并行需手动配置。
实现SchedulingConfigurer接口,在configureTasks方法中配置需要执行的任务和时间间隔。支持通过线程池去启动不同的定时任务。适合定时任务较多的场景。
有些时候我们是需要动态修改执行周期的,那么如何动态修改呢?
对configureTasks里面的方法进行如下改造:
完整代码如下:
查看结果,当一分钟后,任务执行周期调整为每30秒执行一次。
通过接口,动态的启动、修改和删除定时任务,创建一个定时计划ScheduledFuture,在这个方法需要添加两个参数,Runnable(线程接口类) 和CronTrigger(定时任务触发器)在ScheduledFuture中有一个cancel可以停止定时任务。
控制层代码如下:
调用启动接口,postman模拟调用
开始调用任务
控制台查看执行结果,成功启动。
开始执行结果
postman模拟修改接口
调用改变任务周期接口
控制台输出修改,成功修改。
周期改变为20秒一次
以上就是对springboot schedule实现定时任务的总结,当然还有其他方式,比如数据库的方式、XXX-JOB的方式,后续讨论XXX-JOB,不对之处,欢迎指正!
定期更新平常遇到问题的解决方案,不知道大家平常使用的哪种定时任务调度方式,有更好的欢迎推荐、讨论。
C#实现定时器的几种方案
前几天写了一篇java的定时器方案,应小伙伴的要求,今天这里一下c#实现定时器的方案。
在C#里关于定时器类就有三个
1、System.Windows.Forms.Timer
2、System.Threading.Timer
3、定义在System.Timers.Timer
下面对这三个类进行讲解。
System.Windows.Forms.Timer是应用于WinForm中的,它是通过Windows消息机制实现的,类似于VB或Delphi中 的Timer控件,内部使用API SetTimer实现的。它的主要缺点是计时不精确,而且必须有消息循环,Console Application(控制台应用程序)无法使用。
System.Timers.Timer和System.Threading.Timer非常类似,它们都是通过.NET Thread Pool实现的,轻量,计时精确,对应用程序、消息没有特别的要求。System.Timers.Timer还可以应用于WinForm,完全取代上面的System.Windows.Forms.Timer控件。
System.Windows.Forms.Timer
计时器最宜用于 Windows 窗体应用程序中,并且必须在窗口中使用,适用于单线程环境,
在此环境中, UI 线程用于执行处理。 它要求用户代码提供 UI 消息泵, 并且始终从同一线程操作, 或将调用封送到
其他线程。Windows 窗体计时器组件是单线程的, 且限制为55毫秒的准确度,准确性不高
这个是目前我们定时项目中常用的。
这里需要注意的是Execute方法中一定要先关闭定时器,执行完毕后再开启。这个是本人经过测试的,如果你注释掉这两句,定时器会不断的执行Execute方法,如果Execute执行的是一个很耗时的方法,会导致方法未执行完毕,定时器又启动了一个线程来执行Execute方法。
线程计时器也不依赖窗体,是一种简单的、轻量级计时器,它使用回调方法而不是使用事件,并由线程池线程提供支持,先看下面代码
上面是c#定时器的集中方案,大家在使用中一定要尽量把定时器声明成静态(static),如果放在实例方法中,会导致实例对象被回收导致定时器失效。
如果这篇文章对您有帮助,可转发分享一下。
Java定时任务大盘点:发工资也能“指日可待”
作者:京东保险 孙昊宇
让我们先从一个成语开始,“指日可待”。没错,我说的就是定时任务。
“指日可待”: 为任务指定好日程,就可以安心等待任务执行。
在实际场景中,我们往往需要在特定时间做某件事情,或以某个时间间隔重复某件事情,如定期备份数据、定时取消超时订单等。所有和时间有关的事情,都需要借助定时任务来完成。
定时任务可分为两种:本地定时任务、 分布式定时任务。
本地定时任务,即单机定时任务,适合做那些需要每台机器都执行的任务,如刷新每台机器的本地缓存;分布式定时任务则以一个分布式集群为单位执行任务,适用于支持在分布式场景下任务的高可用。
今天让我们看看Java中的本地定时任务,本文将介绍如何使用Timer、ScheduledExecutorService和@Scheduled三种方式实现本地定时任务。
读完本文,你会发现:原来每月最后一个工作日发工资,也可以用定时任务实现!
Timer,即java.util.Timer,是来自Java 1.3的古老定时器。
要使用Timer,要先创建一个TimerTask,作为Timer要执行的任务:
有了Timer和TimerTask,就可以安排任务执行。让我们简单了解下Timer的用法:
使用Timer.schedule方法,只需传入TimerTask和延迟时间,即可让任务在指定的延迟时间后执行一次。也可以传入Date,让任务在指定的时刻执行:
如果直接运行以上代码,会出现“Task already scheduled or cancelled”异常。这是因为一个TimerTask只能被schedule方法调度一次。如果需要执行两个任务,我们需要创建两个TimerTask。我们让两个任务在执行时分别打印当前时刻的秒数,全部代码如下:
运行结果如下,可以看到,task2传入当前时刻立即执行,而task1延迟了5秒执行。
task2开始执行:32 task1开始执行:37
周期性任务可以以固定的周期反复地执行下去。要让Timer周期性执行,同样使用重载的schedule方法,传入第三个参数period——执行周期,就可以让task以固定频率执行。我们给task1传入period = 3000(ms),让它三秒执行一次:
运行结果如下:
task1开始执行:33 task1开始执行:36 task1开始执行:39 task1开始执行:42 … …
Timer是如何实现的?查看Timer的源码,发现Timer有两个成员变量,它们是Timer的核心实现:
TaskQueue:任务队列,其中定义了一个长度为128的TimerTask数组,根据TimerTask.nextExecutionTime(下次执行倒计时)维护成了一个最小堆,堆顶就是最近要执行的任务。
TimerThread:任务触发线程,是一个无限循环的线程,它不断从TaskQueue堆顶取出最近要执行的任务,判断剩余执行时间,等待指定时间后去执行任务。执行时,根据任务配置(单次执行 or 周期执行),决定是否向任务队列中放入下一次任务。
用堆来实现任务优先级队列是非常高效的办法,因为任务触发线程只关心下一个要执行的任务,即堆顶元素,剩下的任务的剩余时间一定更长,不必有序,只需取走堆顶元素后重新堆化即可,每次操作的时间复杂度是O(log n)。
Timer的实现方式,导致其存在如下问题:
(1)Timer只有一个执行任务的线程,即TimerThread。执行任务时其他任务会阻塞,如果一个任务执行很久,会导致后续任务无法按时执行;
(2)Timer内部只捕获了InterruptedException,未捕获运行时异常。如果任务执行过程中抛出运行时异常,线程将直接被杀死,其他任务也将无法执行。
Timer是Java早期的任务调度框架,其缺陷较多,请读者简单了解,非常不建议使用哦。
ScheduledExecutorService,可以称为“计划线程池”或“调度线程池”,来自于Java 1.5的JUC包。作为Java升级版的任务调度框架,它解决了Timer的遗留问题,为多线程场景下的定时任务调度提供了稳定可靠的支持。有了它,再也不需要使用Timer了。
作为Executor框架的一部分,ScheduledExecutorService继承了ExecutorService接口,其实现类为ScheduledThreadPoolExecutor。构造方法有4个,如下:
其中3参数构造方法实现如下:
根据构造方法,可以看到ScheduledThreadPoolExecutor的所有参数如下,其中3个参数可指定,4个参数固定:
① 核心线程数:必传参数,控制执行任务的线程数量;
② 最大线程数:固定为Integer.MAX_VALUE。在ScheduledThreadPoolExecutor中没有作用,实际起作用的是corePoolSize;
③、④ 空闲线程存活时间:固定为0,单位为纳秒。
⑤ 任务队列:固定DelayedWorkQueue延迟阻塞队列,同样是一个最小堆实现的优先级队列。
⑥ 线程工厂:可以手动设置,建议手动传参,方便设置线程名称。
⑦ 拒绝策略:可以手动设置,如果不指定,默认为AbortPolicy,拒绝任务并抛出异常。
让我们看看如何使用它。
要想让任务只执行一次,使用schedule方法即可,有3个参数,依次是:要执行的任务方法(实现Runnable或Callable)、延迟的时间、时间单位。注意,ScheduledExecutorService不支持在指定时刻执行,只能在指定的延迟后执行。示例如下:
周期性执行任务又可细分为:固定频率执行、固定延迟执行。
当以固定频率执行时,以上次任务执行的开始时间到本次任务的开始时间来计算任务周期,不考虑任务的执行时间;
当以固定延迟执行时,以上次任务执行的结束时间到本次任务的开始时间来计算任务周期,即任务的执行时间会影响下次任务的开始时间;
当任务执行时间可以忽略时,两种执行方式效果一样。如果考虑任务的执行时间,如任务周期为5s,任务执行需要1s,那么固定频率执行的效果是:0s(开始)、5s(开始)、10s(开始)… ,而固定延迟执行的效果是:0s(开始)、1s(结束)、6s(开始)、7s(结束)、12s(开始)… 。
要让任务以固定频率执行,使用scheduleAtFixedRate方法。它有4个参数,依次是:要执行的任务方法(实现Runnable或Callable)、首次执行延迟的时间、执行周期、时间单位。
我们设置延迟时间为0,周期为5s,在任务执行中,让任务sleep 1s,模拟任务耗时,配置如下:
效果如下:
创建线程池:4 任务开始执行:4 任务执行完成:5 任务开始执行:9 任务执行完成:10 … …
需要注意的是,如果任务的执行耗时 > 任务周期,即下一个任务要开始时,上一个任务还没结束,则scheduleAtFixedRate并不会严格按照预期时间执行,而是会等待上一个任务执行结束后再执行。即:任何情况下,一个周期任务都不会同时存在两个执行中的任务实例。
要让任务以固定延迟执行,使用scheduleWithFixedDelay方法。其他参数都不变,仅修改方法名,让我们对比下效果:
执行结果如下,可以看到上次任务结束时间和下次任务开始时间的间隔固定,符合我们的预期:
创建线程池:6 任务开始执行:6 任务执行完成:7 任务开始执行:12 任务执行完成:13 … …
首先需要清楚,调度线程池和普通线程池最大的区别是:对普通线程池而言,只要线程池中有空闲工作线程,就只管从任务队列中取出任务执行,因此普通线程池的任务队列都是FIFO的。而调度线程池必须判断每个任务的剩余等待时间,没有“到点”的任务,是不可以执行的。如何合理地安排每个定时任务的执行时间?这就需要特殊的任务队列(优先级队列)了。
上文提到,ScheduledThreadPoolExecutor使用的任务队列固定为:DelayedWorkQueue延迟阻塞队列。这是一个专为ScheduledThreadPoolExecutor定制的、满足多线程定时任务设计的任务队列。具有如下特性:
① 【优先性】优先级队列,实现原理和Timer相同,同样是按照任务剩余时间构造的最小堆,每次从堆顶取得最近要执行的任务;
② 【无限大】初始大小为16,每次队列满后自动扩容,可无限扩容到 Integer.MAX_VALUE,因此在添加任务时(offer方法)不会阻塞;
③ 【并发性】队列操作有锁机制保证线程安全;同时,为了更好管理线程资源,队列采用了Leader-Follower的线程模型。
为了实现该模型,DelayedWorkQueue中定义了如下成员变量:
首先,队列拥有一个重入锁lock,所有队列操作都需要先获取这把锁; 一个成员变量leader,指向下一个要处理队列头部任务的线程,其他空闲的工作线程被称作follower; 最后是lock创建的等待队列available,所有follower都在这里等待,等着成为新的leader。那么什么时候才会出现岗位空缺(available)呢?请看下文。
刚才提到,leader就是处理当前队列头任务的线程。leader首先会判断这个任务的剩余时间,然后等待这个时间。时间一到就取走任务,要去执行,就在leader要“卸任”的时候,它需要通知一下排队的继任者(follower)们,于是发出available.signal()信号,岗位有空缺啦!从而使一个follower线程获取锁成为leader,执行后边任务。最后,所有执行完任务的线程都会重新成为follower等着领新一轮的任务,如此循环。这部分实现,请感兴趣的读者阅读DelayedWorkQueue的take()/poll()方法。
如果在leader等待时,来了新任务怎么办?先别急,重新堆化,如果新任务没排到队首,说明剩余时间肯定大于队首任务,则不需要着急执行;如果新任务排到了队首,说明这个任务时间最紧急,执行时间已经早于了当前leader的苏醒时间了,来不及啦!那么直接把当前的leader踢掉,发送available.signal()信号,召唤新leader执行新任务。这部分实现,请感兴趣的读者阅读DelayedWorkQueue的offer()方法。
这种Leader/Follower模式最早被应用于多线程网络服务中,通过确保请求接收者和执行者是同一个线程来减少接收者另外创建执行者线程的开销,减少线程间数据交换。在DelayedWorkQueue中,这种模式巧妙地确保了在线程池中最多只有一个线程(leader)在等待执行最近的任务,而其他空闲线程可以无限等待直到被唤醒,从而避免多个工作线程同时等待一个任务带来的额外开销。
译自ScheduledThreadPoolExecutor的官方注释:
虽然这个类继承自ThreadPoolExecutor,但继承的一些调优方法对它没有用处。特别是,因为它使用corePoolSize线程和队列充当固定大小的池,所以对maximumPoolSize的调整没有任何有用的影响。此外,将corePoolSize设置为零或使用allowCoreThreadTimeout几乎从来都不是一个好主意,因为这可能会使池中没有线程来处理任务,一旦它们有资格运行。
使用ScheduledThreadPoolExecutor的时候,我们需要格外注意的是:线程池大小始终固定为corePoolSize不会变,而maximumPoolSize没有任何作用,它可不会自己添加工作线程!如果你需要执行多个定时任务,请尽量把corePoolSize设置大一些,避免工作线程不够导致任务没能按时执行。
@Scheduled注解是Spring框架提供的通过注解方式实现定时调度的定时任务框架,来自org.springframework.scheduling包。该注解主要有三种配置定时的方式,分别支持我们以固定频率、固定延迟或cron表达式配置定时任务:
首先让我们看看使用@Scheduled注解需要的配置。要让该注解生效,有两种配置方法。
① 注解启动类:在Spring启动类添加@EnableScheduling注解,才能使@Scheduled生效,默认未开启。
② 配置文件:在配置文件中引入task的命名空间,并添加注解驱动“annotation-driven”:
固定频率执行、固定延迟执行,这两个执行方式前文已经介绍过了,有读者可能要问了,第三种方式cron表达式是什么?又该如何使用呢?
cron始于linux下的定时执行工具,是linux系统的内置服务。在系统中,可以使用crontab命令来设定cron服务,cron会根据命令和执行时间来按时调度工作任务。cron表达式的功能强大,可以满足各种定制化的定时任务配置需求,远比fixedRate、fixedDelay等方式灵活多样,适合各种复杂的定时任务配置。
cron表达式一共包含7个域,每个域代表不同的含义,从左到右依次是:“秒 分 时 日 月 周 年”。这些域以空格隔开组成的字符串就是cron表达式。其中,“年”在大多数场景下用不上(极少有以年为周期的任务),因此不是必要的,前6个域即可组成一个cron表达式。cron表达式的规则是:
① 【数字】在每个域中,可以填入数字,代表在指定的时刻执行。具体到每个域的数字范围是:
分、秒:0-59; 时:0-23; 日:1-31(视每月情况); 月:1-12(JAN-DEC); 周:1-7(SUN-SAT); 年:1970-2099。
特别提醒:在【周】域中,1=周日、2=周一、… 7=周六。这块有点反常识,担心记错的话可以填星期的英文缩写(SUN-SAT)。
② 【*】如果想让这个域无论等于什么值都执行,请填入【*】(通配符)。
这时细心的读者会发现有个bug:如果配置了【日】为【*】,即每天都执行,且【周】配为【6】,即每周五执行,就会出现互斥:又想每天都执行,又想每周五执行,但并非每天都是周五啊! 为了解决这个问题,cron表达式设置了【?】符号。
③ 【?】如果这个域的值不关心,请填入【?】(不指定)。这个符号只能填在【日】或【周】域,用来解决两个日期和星期的互斥问题。
用规则①②③搭配就可以创建很多基本的cron表达式:
1 * * * * ? 每分钟的第1秒执行 0 0 0 * * ? 每天0点执行 0 15 10 ? * * 每天上午10:15执行
光有这些还不够,cron表达式还有更加丰富的符号以满足更多样的需求,请接着往下看:
④ 【-】指定取值范围。例:
0 0 9-17 * * ? 每天9点到17点,每整点执行一次
⑤ 【,】指定多个值。例:
0 0 0 1,15 * ? 每月1日、15日,0点执行
⑥ 【/】指定起始量/增量,以固定频率执行。例:
0/2 * * * * ? (从每分钟的0秒开始)每2秒执行一次 0 0 18/1 * * ? 从每天的18点开始,每整点执行一次
⑦ 【L】即“LAST”之意,只能填在【日】或【周】域,代表最后一天。
在【周】域,还可以写“数字 + L”,代表该取值的最后一个,即最后一个周几,例:
0 0 0 L * ? 每月最后一天0点执行 0 0 0 ? * L 每周最后一天0点执行 0 0 0 ? * WEDL 每月最后一个周三0点执行
⑧【W】表示自动匹配工作日,只能填在【日】域,且必须在数字后。如“5W”意为“本月距离5号最近的工作日”。
也可以将【L】【W】连用,意为“本月最后一个工作日”。
0 0 0 1W * ? 在本月距离1号最近的工作日0点执行 0 0 0 LW * ? 在本月最后一个工作日0点执行 看到这里,我想你已经明白怎么用定时任务发工资了。这个表达式,真像是为月末发工资准备的呢!
⑨【#】指定第几个周几,只能填在【周】域。【#】左边填周几,右边填第几个。例:
0 0 0 ? 5 SUN#2 每年母亲节(5月的第二个星期日)的0点执行
强大的cron表达式几乎可以满足所有定时任务的需求,如果你迫不及待想尝试一下cron表达式并验证效果,推荐这个网站:https://cron.qqe2.com,可以在线生成和验证cron表达式,并看到预期执行结果。
看完了cron表达式,那么问题来了:@Scheduled注解实现的定时任务是单线程还是多线程呢?如果是多线程,有几个线程?如何控制线程池?
很遗憾,答案是:在默认配置下,@Scheduled注解是单线程的。这是因为@Scheduled注解默认创建的线程池大小为1,这显然很可能导致阻塞问题。要想改成多线程,需要手动配置任务线程池,让我们一起看看。
方法①:使用.properties文件或.yml文件,直接配置线程池大小
方法②:手动配置线程池org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler,并设置线程池大小等参数。ThreadPoolTaskScheduler即为@Scheduled注解所创建的线程池:
经过以上配置,我们可以让@Scheduled注解的定时任务以多线程方式执行,即使A任务阻塞,B任务也不会受影响。这和上文提到的ScheduledExecutorService的效果是完全一样的。事实上,ThreadPoolTaskScheduler就是基于ScheduledThreadPoolExecutor实现的,其部分源码:
不难发现,Spring的ThreadPoolTaskScheduler就是在ScheduledThreadPoolExecutor基础上封装了一层,且默认的线程池大小为1。在原线程池的基础上增加了注解驱动、cron表达式解析等功能,更加方便了我们的使用。
以上提到的所有任务调度都有一个共同点:我们可以通过线程池让不同任务的执行互不干扰,但对于同一任务,当上一次执行未完成时,即使到了下一次执行时间,下一次执行还是会等待,即不会出现一个任务同时存在两个执行中的任务实例。如果想让一个任务的每次执行都互不影响呢?
@Async注解可以帮助我们,它可以支持异步地执行方法,每次执行都会另起线程。我们将它和@Scheduled注解一起加在方法上:
这样,就可以让该任务的每次执行都互不影响。我们还可以在注解中指定异步方法执行的线程池,如@Async(\”asyncExecutor\”),且asyncExecutor应为ThreadPoolTaskScheduler类型。
⚠ 特别提醒:原则上讲,不应该,也没有必要让一个周期性任务异步执行。一旦允许异步,如果该任务卡死,后续本类任务不再阻塞,还会继续起新线程,并不断卡死,很快把任务线程池打满,最后阻塞所有的定时任务,造成严重后果。如果一定要使用,请为异步定时任务手动指定一个单独的任务线程池,并配置好最大等待时长(setAwaitTerminationSeconds),避免无限阻塞。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。