1、定时任务概述
定时任务是一种自动化执行特定操作的方式,可以根据预定的时间、日期或间隔周期性地执行某些任务。
定时任务的作用?
自动化任务执行:定时任务能够在预定的时间触发执行某些任务,无需人工干预。这对于需要定期执行的重复性任务非常有效,例如数据备份、统计报表生成、系统维护等。
提高效率和准确性:通过定时任务,可以在特定的时间段内自动执行任务,避免了人工操作的疏忽和错误。这样可以提高任务的执行效率和准确性,并降低因人为原因导致的错误风险。
节省时间和资源:定时任务可以代替人工手动执行的操作,节省了大量人力资源和时间成本。同时,它也可以合理分配系统资源,避免任务集中导致的系统负载过高。
异步执行:定时任务可以在后台异步执行,不会阻塞用户的其他操作。这对于需要执行耗时较长的任务或需要长时间运行的操作非常有用,可以提高系统的响应速度和用户体验。
2、定时任务的常见几种方式
1)线程类实现定时任务:比如Thread、Runnable、Callable等线程类都可以实现定时任务
2)Timer/TimerTask:Java提供了java.util.Timer和java.util.TimerTask类,可以用于创建定时任务。通过创建一个Timer对象,并调用其schedule()方法,可以指定任务的执行时间和执行间隔。然后,创建一个继承自TimerTask的子类,实现具体的任务逻辑,并在run()方法中定义需要执行的代码。最后,将该任务对象通过Timer的schedule()方法进行调度即可。
3)ScheduledExecutorService:Java提供了java.util.concurrent.ScheduledExecutorService接口,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。可以用于创建定时任务。通过调用ScheduledExecutorService的scheduleAtFixedRate()或scheduleWithFixedDelay()方法,可以指定任务的执行时间和执行间隔。然后,创建一个实现了Runnable接口的类,实现具体的任务逻辑,并在run()方法中定义需要执行的代码。最后,将该任务对象提交给ScheduledExecutorService进行调度即可。
4)@Scheduled注解:这个是Spring框架所提供的,通过在方法上添加@Scheduled注解,并设置相应的时间表达式,就可以让方法按照指定的时间间隔自动执行。
3、timer实现定时任务
3.1)timer实现定时任务案例
// 创建一个Timer对象 Timer timer = new Timer();创建一个TimerTask对象,并重写其run方法 TimerTask timerTask = new TimerTask() {int count = 0; @Overridepublic void run() {//执行定时任务的逻辑count++; System.out.println("定时任务执行次数: " + count); // 当count等于5时,打印出定时任务执行完毕,并调用timer.cancel()取消定时任务 if (count == 5) { System.out.println("定时任务执行完毕"); timer.cancel(); } }};// 延迟1秒后开始执行定时任务,每隔2秒执行一次 timer.schedule(task, 1000, 2000);
以上案例代表延迟1秒后执行输出次数,每2秒执行1次,输出如下:
定时任务执行次数: 1
定时任务执行次数: 2
定时任务执行次数: 3
定时任务执行次数: 4
定时任务执行次数: 5
定时任务执行完毕
3.2)Timer的常用方法
schedule(TimerTask task, Date time):在指定的时间执行任务。参数task是要执行的任务,参数time是任务的执行时间。
schedule(TimerTask task, long delay):在指定的延迟时间后执行任务。参数task是要执行的任务,参数delay是任务的延迟时间(单位为毫秒)。
schedule(TimerTask task, long delay, long period):在指定的延迟时间后开始执行任务,并按照指定的周期重复执行。参数task是要执行的任务,参数delay是任务的延迟时间(单位为毫秒),参数period是任务的执行周期(单位为毫秒)。
scheduleAtFixedRate(TimerTask task, long delay, long period):在指定的延迟时间后开始执行任务,并以固定的速率重复执行。参数task是要执行的任务,参数delay是任务的延迟时间(单位为毫秒),参数period是任务的执行周期(单位为毫秒)。该方法会尽量保持每次任务执行的时间间隔固定。
cancel():取消所有已安排的任务。调用该方法后,Timer将不再接受新任务,并尝试终止当前正在执行的任务。
3.3)timer的优缺点
优点:JDK自带的,简单易用
缺点:
对系统时间敏感:Timer类的任务调度是基于绝对时间的,而不是相对时间。这意味着对系统时间的改变非常敏感,当系统时间发生变化时,可能导致任务执行时间的误差。
单线程执行:Timer类内部使用单个线程来执行所有的定时任务。如果某个任务执行时间过长,会影响其他任务的执行,可能导致任务被延迟。
错误处理能力有限:Timer类的错误处理能力较弱。如果定时任务出现异常并抛出未捕获的异常,Timer类将会停止所有任务的执行。
任务的无法持久化:当应用程序关闭或重启时,Timer 中已经调度的任务会丢失
不适合高并发场景:由于Timer类使用单个线程执行所有任务,不适合在高并发环境下使用。当任务过多或任务执行时间较长时,会影响整体性能和响应性。
3.4)timer的异常捕获
Timer线程是不会捕获异常的,如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,它会错误的认为整个Timer线程都会取消。同时,已经被安排单尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。
例如:
Timer timer = new Timer();TimerTask timerTask = new TimerTask() {@Overridepublic void run() {int x = 1/0;System.out.println("执行定时任务");}};timer.scheduleAtFixedRate(timerTask, 5 * 1000, 10 * 1000);
执行结果如下:
因此如果要防止使用timer因未捕获的异常而使得任务停止,可以在重写run方法内进行异常捕获,修改代码如下:
Timer timer = new Timer();TimerTask timerTask = new TimerTask() {@Overridepublic void run() {try{System.out.println("定时任务开始");int x = 1/0;System.out.println("执行定时任务");}catch(Exception e){System.out.println("异常捕获");}}};
运行结果如下:
3.5)timer任务传入参数实现
public class task extends TimerTask{private int x;public task(int x) {this.x = x;}@Overridepublic void run() {//执行定时任务的逻辑System.out.println(x);}public static void main(String[] args) {Timer timer = new Timer();timer.scheduleAtFixedRate(new task(3), 5 * 1000, 10 * 1000);}}
3.6)Timer和TimerTask之间的联系和区别
1) 定义与功能
-
TimerTask类
- TimerTask是一个抽象类,它实现了Runnable接口的run()方法。
- 主要功能:定义要执行的具体任务逻辑。它本身不执行任何任务,而是等待Timer来调度执行。
-
Timer类
- Timer是一个工具类,用于调度一个线程以在将来某个时间执行指定的任务。
- 主要功能:作为定时器,负责调度和管理TimerTask任务。它可以安排任务执行一次,或者定期重复执行。
2) 协作方式
- Timer和TimerTask通常成对出现。Timer作为定时器,负责调度和管理TimerTask任务;而TimerTask则是定时任务的具体实现,由Timer来调度执行。
- 一个Timer可以调度任意多个TimerTask,所有任务都存储在一个队列中顺序执行。如果需要多个TimerTask并发执行,则需要创建多个Timer实例(因为每个Timer仅对应一个线程)。
3) 使用场景与特点
-
TimerTask
- 适用于需要在指定时间执行任务的场景。
- 提供了简单的接口,使得任务的调度变得简单。
- 可以根据需要设置任务的执行时间和执行周期。
-
Timer
- 适用于需要调度和管理多个定时任务的场景。
- 线程安全,但不保证任务执行的精确性(因为基于单线程执行)。
- 如果某个任务很耗时,可能会影响其他计划任务的执行。因此,在JDK 1.5及以上版本中,建议使用ScheduledThreadPoolExecutor来代替Timer执行计划任务。
4) 异常处理与线程管理
-
异常处理
Timer线程不会捕获异常。如果TimerTask抛出了未检查的异常,会终止Timer线程,从而导致其他计划任务无法得到继续执行。因此,在TimerTask的run()方法中捕获所有可能的异常是非常重要的。 -
线程管理
- Timer内部使用了一个线程(TimerThread)来顺序执行所有的TimerTask任务。
- 可以通过调用Timer.cancel()方法来终止Timer线程。另外,如果创建Timer时将其设置为守护线程(使用new Timer(true)),则当且仅当进程结束时,该守护线程会自动注销。
4、ScheduledExecutorService实现定时任务
ScheduledExecutorService
是 Java 中用于调度任务的接口,它继承自 ExecutorService
,提供了在给定延迟后运行任务、定期执行任务等能力
4.1)ScheduledExecutorService实现定时任务案例
// 创建一个ScheduledThreadPoolExecutor,这里使用1个线程
// ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);// 创建一个实现了Runnable接口的任务Runnable task = new Runnable() {@Overridepublic void run() {//执行定时逻辑System.out.println();}};// 安排任务在初始延迟后执行,然后每隔指定的周期重复执行(这里使用不重复执行作为示例)// 如果需要重复执行,请使用scheduleAtFixedRate或scheduleWithFixedDelay方法
// executor.schedule(task, 0, TimeUnit.SECONDS); // 立即执行,延迟时间为0秒executor.scheduleWithFixedDelay(task, 5, 10, TimeUnit.SECONDS);// 注意:在实际应用中,您可能需要在某个时刻关闭executor,以避免资源泄露// 这通常是在应用程序关闭或不再需要定时任务时进行的// 例如,您可以在main方法的最后添加以下代码(但这将阻止程序立即退出):// executor.shutdown();// 如果希望等待所有任务完成后再关闭,可以使用:// executor.shutdownNow(); // 或者更优雅地等待任务完成:executor.awaitTermination(...);
4.2)ScheduledExecutorService的常用方法
schedule(Runnable command, long delay, TimeUnit unit): 在指定的延迟时间后执行一次任务。
schedule(Callable<V> callable, long delay, TimeUnit unit): 在指定的延迟时间后执行一次任务,并返回一个可获取结果的 Future 对象。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 在初始延迟时间后开始执行任务,并以固定的时间间隔重复执行任务。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): 在初始延迟时间后开始执行任务,并在每次任务完成后延迟指定的时间再执行下一次任务。
submit(Callable<V> task): 提交一个可获取结果的任务,并返回一个表示任务执行结果的 Future 对象。
submit(Runnable task): 提交一个不返回结果的任务,并返回一个表示任务执行完成的 Future 对象。
shutdown(): 优雅地关闭 ScheduledExecutorService,等待已提交的任务执行完毕。
shutdownNow(): 强制关闭 ScheduledExecutorService,立即停止所有任务的执行。
注意:
虽然使用ScheduledExecutorService实现定时任务如果未捕获异常线程不会像用timer实现定时任务而终止,但是如果遇到了未捕获的异常,此定时任务会停止执行,不会开始下一次定时任务,线程会阻塞在异常处,因此即使使用ScheduledExecutorService实现定时任务也建议在run方法内捕获异常,保证即使这次遇到异常,下次任务能够定时执行。
4.3)Timer VS ScheduledExecutorService
Timer
和 ScheduledExecutorService
都是 Java 中用于执行定时任务的工具,但它们之间存在一些关键差异。以下是对这两者的详细比较:
1)线程模型
- Timer
Timer
是单线程的,每创建一个Timer
实例,就会创建一个新的线程(TimerThread
)来执行任务。- 如果一个
TimerTask
执行的时间过长,它会独占Timer
对象,导致后续的任务无法及时执行,必须等待当前任务完成后才能继续。 Timer
默认情况下不是守护线程,但可以设置为守护线程(new Timer(true)
)。当进程中没有其他非守护线程时,守护线程将销毁。
- ScheduledExecutorService
ScheduledExecutorService
是基于线程池实现的,支持多个任务并发执行。- 线程池中的线程数量可以根据需求进行配置,因此可以同时执行多个任务,而不会相互阻塞。
ScheduledExecutorService
提供了更灵活和强大的线程管理功能。
2)任务调度
- Timer
Timer
对调度的支持是基于绝对时间的,因此任务对系统时钟的改变是敏感的。- 如果
TimerTask
抛出未检查的异常,Timer
将会产生无法预料的行为。具体来说,Timer
线程并不捕获异常,所以TimerTask
抛出的未检查的异常会终止Timer
线程。此时,已经被安排但尚未执行的TimerTask
永远不会再执行了,新的任务也不能被调度。
- ScheduledExecutorService
ScheduledExecutorService
只支持相对时间进行调度。- 它提供了
scheduleAtFixedRate
和scheduleWithFixedDelay
两种方法,允许更灵活地安排任务的执行。 - 如果任务抛出异常,它不会影响其他任务的执行(除非异常导致整个线程池被关闭)。
3)任务取消与资源管理
- Timer
Timer
中的cancel()
方法可以将任务队列中的全部任务进行取消,但有时并不一定停止任务,因为Timer
类中的cancel()
方法有时并没有竞争到锁。- 由于
Timer
是单线程的,取消任务时可能会遇到一些竞争条件,导致任务取消不完全或延迟。
- ScheduledExecutorService
- 通过
ScheduledExecutorService
安排的任务可以通过Future
对象进行取消。调用Future
对象的cancel
方法可以请求取消执行此任务。 ScheduledExecutorService
提供了更细粒度的任务管理功能,可以单独取消某个任务,而不会影响其他任务的执行。- 当不再需要使用
ScheduledExecutorService
时,应该及时关闭它以释放系统资源。可以使用shutdown
或shutdownNow
方法来关闭线程池。
- 通过
4)使用建议
- 如果需要执行简单的、少量的定时任务,并且不需要并发执行,那么
Timer
可能是一个合适的选择。 - 如果需要执行大量的定时任务,或者需要并发执行多个任务,那么
ScheduledExecutorService
是更好的选择。它提供了更强大的线程管理功能和更灵活的任务调度能力。
5、@Scheduled注解实现定时任务
在Spring框架中,@Scheduled
注解提供了一种非常方便的方式来实现定时任务。
5.1)@Scheduled注解定时任务实现案例
首先,确保你的Spring项目已经包含了必要的依赖项,比如spring-context
和spring-scheduling
。如果你使用的是Maven项目,可以在pom.xml
中添加以下依赖(如果Spring Boot已经包含了这些依赖,则无需重复添加):
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>你的Spring版本</version>
</dependency>
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-scheduling</artifactId> <version>你的Spring版本</version>
</dependency>
然后,在你的Spring配置类(通常是带有@Configuration
注解的类)或Spring Boot的主类上添加@EnableScheduling
注解,以启用Spring的计划任务功能:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling; @Configuration
@EnableScheduling
public class SchedulingConfig { // 这里可以添加其他配置
}
接下来,创建一个包含定时任务的类,并使用@Scheduled
注解来标记定时方法。例如:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; @Component
public class ScheduledTasks { // 每5秒执行一次 @Scheduled(fixedRate = 5000) public void reportCurrentTime() { System.out.println("当前时间: " + System.currentTimeMillis()); } // 每天凌晨1点执行一次(需要cron表达式) @Scheduled(cron = "0 0 1 * * ?") public void executeDailyTask() { System.out.println("执行每日任务: " + System.currentTimeMillis()); }
}
5.2)@Scheduled注解属性介绍
cron():用于指定Cron表达式,表示任务的执行时间规则。例如0 0 * * * ?表示每天的凌晨12点执行一次任务。
zone():用于指定Cron表达式的时区,默认为空字符串。如果需要根据不同的时区执行任务,则可以设置该属性。
fixedDelay()和fixedDelayString():用于指定任务的固定延迟时间,即任务结束后等待多长时间再次执行。默认值为-1,表示不使用固定延迟。
fixedRate()和fixedRateString():用于指定任务的固定频率,即任务开始执行后多长时间再次执行。默认值为-1,表示不使用固定频率。
initialDelay()和initialDelayString():用于指定任务的初始延迟时间,即任务首次执行前等待多长时间。默认值为-1,表示立即执行。
timeUnit():用于指定时间单位,可选值有TimeUnit.MILLISECONDS(毫秒,默认值)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等
fixedDelay + initialDelay 属性功能上就等价于 Timer 的schedule方法了,
fixedRate+initialDelay属性功能上就等价于 Timer 的 scheduleAtFixedRate 方法了
6、分布式定时任务
前面所有的定时任务,无论是基于线程类,还是基于 JDK 自带的定时任务,还是基于Spring提供的Spring Task,都无法在分布式环境下使用,并且不支持持久化,一旦服务重启所有的定时任务都将发生丢失,所以我们需要使用到其它的第三方成熟的定时任务框架。
Quartz:是一个功能强大的开源作业调度框架,用于在Java应用程序中实现定时任务调度和作业调度。
XXL-Job:是一个轻量级分布式任务调度平台。特点是平台化,易部署,开发迅速、学习简单、轻量级、易扩展。由调度中心和执行器功能完成定时任务的执行。调度中心负责统一调度,执行器负责接收调度并执行。
Elastic-Job:是一个开源的分布式任务调度解决方案,它是基于Java的轻量级分布式调度框架
三者的比较
功能和特性: Quartz:Quartz是一个功能强大的作业调度框架,支持灵活的任务调度策略、分布式集群、任务持久化等特性。它具有丰富的API和扩展点,可以根据需求进行定制开发和扩展。 XXL-Job:XXL-Job是一个分布式任务调度平台,提供了可视化操作界面、多种任务调度方式、分片任务支持等特性。它注重于任务的管理和监控,并提供了报警与告警功能。 Elastic-Job:Elastic-Job是一个轻量级的分布式任务调度解决方案,支持分布式任务调度、弹性扩缩容、任务监控和管理等特性。它注重于任务的弹性扩展和容错机制。
分布式支持: Quartz:Quartz在分布式场景中需要基于数据库锁来保证操作的唯一性,通过多个节点的异步运行实现高可用性。但它没有执行层面的任务分片机制。 XXL-Job:XXL-Job提供了分布式集群的支持,可以实现任务的负载均衡和高可用性。它支持分片任务和动态调整任务节点数量的特性。 Elastic-Job:Elastic-Job支持分布式任务调度,具备弹性扩缩容能力,可以根据任务的执行情况动态调整任务节点数量。
可视化和管理界面: Quartz:Quartz本身没有提供可视化的任务管理界面,需要通过其他工具或自行开发来实现。 XXL-Job:XXL-Job提供了简洁直观的任务管理界面,方便用户进行任务的创建、编辑、状态查看等操作。 Elastic-Job:Elastic-Job提供了任务监控和管理功能,可以查看任务的执行日志、运行状态、统计信息等。
社区活跃度和生态系统: Quartz:Quartz是一个非常成熟且广泛使用的作业调度框架,拥有强大的社区支持和丰富的生态系统。 XXL-Job:XXL-Job也有一个活跃的社区,并且在国内得到广泛应用和认可。 Elastic-Job:Elastic-Job相对较新,并且社区规模较小,但其在分布式任务调度领域有一定的影响力。
Quartz在功能和扩展性上非常强大,适用于复杂的任务调度需求。XXL-Job注重于任务管理和监控,并提供了可视化的操作界面。Elastic-Job轻量级且具备分布式任务调度和弹性扩缩容能力。
总结
线程+休眠实现定时任务,是最简单实现定时任务的方式了,但这只是提供一种思路,实际开发中几乎不会使用
JDK自带的定时任务Timer和ScheduledExecutorService,我们需要了解两者的区别
Timer是单线程的,一旦发生异常,将终止所有的任务;Timer是绝对时间的,会受到系统时间的影响 ScheduledExecutorService是基于线程池,是多线程的,一旦发生异常,不会终止所有的任务;ScheduledExecutorService是相对时间 ,不会受到系统时间的影响 注意区固定间隔和固定频率的区别 Spring Task实现的定时任务是基于线程池,是多线程的,一旦发生异常,不会终止所有的任务;基于相对时间,不会受到系统时间的影响
分布式定时任务,一般是直接使用第三方成熟的定时任务框架,当然如果你公司资金充足可以选择开发定制化定时任务框架。选用开源的第三方成熟定时任务框架,好处在于功能完善、免费,代码质量也是有保障的