Java 中实现定时任务的几种方式
在 Java 中,实现定时任务主要有以下几种方式,每种方式适用于不同的场景:
1. Timer
和 TimerTask
(JDK 原生)
Java中的Timer类是一个定时调度器,用于在指定的时间点执行任务。
特点:
Timer 类用于实现定时任务,最大的好处就是他的实现非常简单,特别的轻量级,因为它是Java内置的,所以只需要简单调用就行了。
-
简单易用,适合单线程任务调度。
-
基于绝对时间(
Date
)执行任务。
示例:
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("Task executed at: " + new Date()); } }, 1000, 2000); // 延迟 1s 后执行,之后每 2s 执行一次
缺点:
-
Timer内部是单线程执行任务的,如果某个任务执行时间较长,会影响后续任务的执行。
-
如果任务抛出未捕获异常,将导致整个 Timer 线程终止,影响其他任务的执行。
-
Timer 无法提供高精度的定时任务。因为系统调度和任务执行时间的不确定性,可能导致任务执行的时间不准确。
-
虽然可以使用 cancel 方法取消任务,但这仅仅是将任务标记为取消状态,仍然会在任务队列中占用位置,无法释放资源。这可能导致内存泄漏。
-
当有大量任务时,Timer 的性能可能受到影响,因为它在每次扫描任务队列时都要进行时间比较。
-
Timer执行任务完全基于JVM内存,一旦应用重启,那么队列中的任务就都没有了
2. ScheduledExecutorService
(JDK 1.5+,推荐)
特点:
-
基于线程池(
ThreadPoolExecutor
),支持多任务并发执行。 -
相比
Timer
,更稳定、更灵活。
示例:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 延迟 1s 后执行 scheduler.schedule(() -> { System.out.println("Task executed once at: " + new Date()); }, 1, TimeUnit.SECONDS); // 延迟 1s 后执行,之后每 2s 执行一次 scheduler.scheduleAtFixedRate(() -> { System.out.println("Fixed-rate task executed at: " + new Date()); }, 1, 2, TimeUnit.SECONDS);
优点:
-
线程池管理:避免单线程阻塞问题。
-
支持
scheduleAtFixedRate
(固定频率)和scheduleWithFixedDelay
(固定间隔)。
scheduleAtFixedRate(固定频率)和 scheduleWithFixedDelay(固定间隔)有什么区别
如下表,主要就这些区别
-
用
scheduleAtFixedRate
:
当任务执行时间 短于周期,且需要 严格保证频率(如每 5 秒采集一次指标)。 -
用
scheduleWithFixedDelay
:
当任务执行时间 不确定,且需要 稳定间隔(如每次任务完成后间隔 2 秒再执行)。
对比项 | scheduleAtFixedRate (固定频率) | scheduleWithFixedDelay (固定间隔) |
---|---|---|
触发时机 | 按任务开始时间计算 | 按任务结束时间计算 |
任务执行时间 > 周期 | 后续任务延迟(但频率不变) | 后续任务按固定间隔触发(间隔稳定) |
适用场景 | 需要严格保证执行频率(如定时心跳) | 需要稳定间隔(如资源清理、轮询检测) |
是否可能并发执行 | 不会(单线程顺序执行) | 不会 |
-
scheduleAtFixedRate
:-
固定频率,适合短期任务(如定时上报数据)。
-
不关心任务执行时间,只保证 计划执行时间。
-
-
scheduleWithFixedDelay
:-
固定间隔,适合耗时任务(如文件处理)。
-
保证 两次任务结束之间的间隔 稳定。
-
-
线程池大小:
-
如果任务耗时较长,需增加线程池大小(否则任务会堆积)。
-
示例:
Executors.newScheduledThreadPool(2)
。
-
-
异常处理:
-
如果任务抛出异常,后续任务 会被取消!需在任务内捕获异常。
-
-
任务取消:
-
通过
Future.cancel()
可取消周期性任务。
-
3. Spring Task
(Spring 框架支持)
特点:
-
基于
ScheduledExecutorService
,提供注解式任务调度。 -
适合 Spring/Spring Boot 项目。
示例:
@Component public class MyScheduledTask { @Scheduled(fixedRate = 2000) // 每 2s 执行一次 public void runTask() { System.out.println("Spring Task executed at: " + new Date()); } @Scheduled(cron = "0/5 * * * * ?") // 每 5s 执行一次(Cron 表达式) public void runCronTask() { System.out.println("Cron Task executed at: " + new Date()); } }
配置(Spring Boot):
@EnableScheduling // 启用定时任务 @SpringBootApplication public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } }
优点:
-
注解驱动,开发简单。
-
支持
Cron
表达式(如0 0 12 * * ?
表示每天中午 12 点执行)。 -
Cron 表达式语法:
秒 分 时 日 月 周 [年](可选)
-
*
:任意值(如* * * * * ?
表示每秒执行一次)。 -
,
:枚举(如0 0 12,18 * * ?
表示每天 12 点和 18 点执行)。 -
-
:范围(如0 0 9-17 * * ?
表示 9:00-17:00 每小时执行)。 -
/
:间隔(如0/5 * * * * ?
表示每 5 秒执行一次)。
cron可以定时执行其实底层也是基于操作系统的定时器的机制。在常见的计算机操作系统中,都提供了一种定时器机制,可以设置定时器来触发某个操作或执行某个任务。Cron 表达式利用这种机制实现了定时执行任务的功能。
4. Quartz
(企业级任务调度框架)
基于时间轮算法实现的。时间轮算法是一种时间管理算法,可以高效地处理定时任务。它将时间划分成若干个时间槽,并使用循环队列来存储在每个时间槽上触发的任务,从而避免了遍历整个定时事件集合的开销。
如Netty中的HashedWheelTimer、Quartz Scheduler、Kafka中等都有时间轮算法的应用。
示例:
// 1. 定义 Job public class MyJob implements Job { @Override public void execute(JobExecutionContext context) { System.out.println("Quartz Job executed at: " + new Date()); } } // 2. 配置 Trigger 和 Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(MyJob.class) .withIdentity("myJob", "group1") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(5) // 每 5s 执行一次 .repeatForever()) .build(); scheduler.scheduleJob(job, trigger); scheduler.start();
优点:
-
支持分布式任务调度(如数据库存储任务状态)。
-
动态调整任务(如暂停、恢复、修改执行时间)。
什么是时间轮算法
时间轮可以看作一个 环形数组(类似钟表),每个槽(Bucket)对应一个时间间隔,存放该间隔内到期的任务。
(1)时间轮的基本结构
时间轮算法主要需要定义一个时间轮盘,在一个时间轮盘中划分出多个槽位,每个槽位表示一个时间段,这个段可以是秒级、分钟级、小时级等等。如以下就是把一个时间轮分为了60个时间槽,每一个槽代表一秒钟。
比如当前如果是0秒,那么要3秒后执行,那就挂在槽位为3的那个位置上。而随着时间的推移,轮盘不断旋转,任务会被定期触发。
因为这个时间轮是60个槽位,那么他就会在一分钟完整的转完一圈,那么就有一个指针,每一秒钟在槽位中进行一次移动。这个操作是有一个单独的线程来做的,他的工作就是每一秒钟改变一次current指针。
(2)关键参数
参数 | 说明 |
---|---|
tickMs | 一个槽代表的时间跨度(如 1ms、100ms)。 |
wheelSize | 环形数组的大小(槽数),决定时间轮的周期(tickMs × wheelSize )。 |
currentTime | 当前指针指向的槽,随时间推移移动。 |
(3)任务触发流程
-
任务根据到期时间被分配到对应的槽。
-
时间轮指针每隔
tickMs
移动一格。 -
执行当前槽中的所有任务。
-
实现 O(1) 复杂度的定时任务管理。
分层时间轮:在分层时间轮包括多个级别的时间轮,每个级别的时间轮都有不同的粒度和周期
主要是解决一个时间表示不了的时间,假设间隔1s,60个槽位,但是我要实现了120s的定时任务一个时间轮就不够用了,这时候就需要分层了,我们在刚刚的秒级时间轮的基础上,在定义一个分钟级时间轮。也就是说我们对于120s以后执行这个任务,我们先把他放到分钟级时间轮上,这个时间轮的槽位每一分钟移动一次,当移动时候,发现某个槽位上有这一分钟内需要执行的任务时。把这个任务取出来,放到秒级时间轮中。这样在第3分20秒的时候,就可以运行这个任务了。
-
适用场景:
-
高频定时任务(如心跳检测)。
-
需要低延迟的任务触发(如超时控制)。
-
5. DelayQueue 阻塞队列
DelayQueue是一个带有延迟时间的无界阻塞队列,它的元素必须实现Delayed接口。当从DelayQueue中取出一个元素时,如果其延迟时间还未到达,则会阻塞等待,直到延迟时间到达。因此,我们可以通过将任务封装成实现Delayed接口的元素,将其放入DelayQueue中,再使用一个线程不断地从DelayQueue中取出元素并执行任务,从而实现定时任务的调度。
DelayQueue
是 Java 并发包 (java.util.concurrent
) 中的一个 无界阻塞队列,用于存放实现了 Delayed
接口的元素,只有当元素的延迟时间到期时才能被取出。它非常适合实现 精确的定时任务调度。
1. DelayQueue
核心特性
特性 | 说明 |
---|---|
基于优先级队列 | 内部使用 PriorityQueue 小顶堆数据结构存储元素,按到期时间排序。 |
线程安全 | 通过 ReentrantLock 保证并发安全。 |
阻塞操作 | take() 方法会阻塞,直到有元素到期;poll() 非阻塞,立即返回。 |
适用场景 | 定时任务调度、缓存过期清理、延迟重试机制等。 |
2. 实现步骤
(1)定义 Delayed
任务对象
任务类需要实现 Delayed
接口,并重写 getDelay()
和 compareTo()
方法:
import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayedTask implements Delayed { private final String taskName; private final long executeTime; // 执行时间(毫秒时间戳) public DelayedTask(String taskName, long delayMs) { this.taskName = taskName; this.executeTime = System.currentTimeMillis() + delayMs; } @Override public long getDelay(TimeUnit unit) { // 返回剩余延迟时间 long remainingDelay = executeTime - System.currentTimeMillis(); return unit.convert(remainingDelay, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed other) { // 按执行时间排序(早到期者优先) return Long.compare(this.executeTime, ((DelayedTask) other).executeTime); } public void execute() { System.out.println("Executing task: " + taskName + " at " + new Date(executeTime)); } }
(2)创建 DelayQueue
并提交任务
import java.util.concurrent.DelayQueue; public class DelayQueueScheduler { private static final DelayQueue<DelayedTask> queue = new DelayQueue<>(); public static void main(String[] args) throws InterruptedException { // 提交延迟任务 queue.put(new DelayedTask("Task 1", 3000)); // 3s 后执行 queue.put(new DelayedTask("Task 2", 1000)); // 1s 后执行 queue.put(new DelayedTask("Task 3", 5000)); // 5s 后执行 // 启动消费者线程处理任务 while (!queue.isEmpty()) { DelayedTask task = queue.take(); // 阻塞直到有任务到期 task.execute(); } } }输出:
Executing task: Task 2 at [当前时间 + 1s] Executing task: Task 1 at [当前时间 + 3s] Executing task: Task 3 at [当前时间 + 5s]
3. 关键点解析
(1)Delayed
接口方法
方法 | 作用 |
---|---|
getDelay(TimeUnit) | 返回剩余延迟时间(到期时间 - 当前时间),队列据此判断元素是否可取出。 |
compareTo(Delayed) | 定义优先级,确保最早到期的任务排在队列头部。 |
(2)DelayQueue
的核心方法
方法 | 行为 |
---|---|
put(task) | 插入任务(非阻塞)。 |
take() | 阻塞 取出已到期的任务,若无任务到期则等待。 |
poll() | 非阻塞 取出已到期的任务,若无任务到期则返回 null 。 |
peek() | 查看队首任务(不移除),可能返回未到期的任务。 |
4. 高级用法
(1)动态添加任务
// 在另一个线程中添加新任务 new Thread(() -> { try { Thread.sleep(2000); queue.put(new DelayedTask("Dynamic Task", 2000)); // 2s 后执行 } catch (InterruptedException e) { e.printStackTrace(); } }).start();
(2)结合线程池处理任务
ExecutorService executor = Executors.newFixedThreadPool(2); while (true) { DelayedTask task = queue.take(); executor.submit(task::execute); // 异步执行任务 }
(3)超时控制
DelayedTask task = queue.poll(1, TimeUnit.SECONDS); // 最多等待 1s if (task != null) { task.execute(); }
5. 对比其他定时任务方案
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
DelayQueue | 精确延迟、无第三方依赖 | 单机使用 | 简单延迟任务 |
ScheduledThreadPoolExecutor | 线程池管理、支持周期性任务 | 不适用于动态调整延迟 | 固定频率任务 |
Quartz | 分布式、持久化、复杂调度 | 重量级 | 企业级任务调度 |
Spring @Scheduled | 注解驱动、易用 | 依赖 Spring 生态 | Spring Boot 项目 |
6. 典型应用场景
-
订单超时取消
-
下单后将订单放入
DelayQueue
,30 分钟后检查是否支付。
-
-
缓存过期清理
-
缓存项设置 TTL,到期后自动移除。
-
-
延迟重试机制
-
失败任务延迟 5 秒后重新执行。
-
7. 总结
-
DelayQueue
是一个基于优先级队列的延迟任务调度器,适合单机环境下的精确延迟任务。 -
核心步骤:
-
实现
Delayed
接口定义任务。 -
使用
put()
提交任务,take()
取出到期任务。
-
-
优势:轻量级、无外部依赖、延迟精度高。
-
局限:不支持分布式,适合小型应用或辅助其他调度框架。
6:xxl-job:
xxl-job是一款分布式定时任务调度平台,可以实现各种类型的定时任务调度,如定时执行Java代码、调用HTTP接口、执行Shell脚本等。xxl-job采用分布式架构,支持集群部署,可以满足高并发、大数据量的任务调度需求。
二、为什么定时任务可以定时执行?
定时任务可以定时执行的原理是通过操作系统提供的定时器实现的。定时器是计算机系统的一个重要组成部分,它可以周期性地发出信号或中断,以便操作系统或其他应用程序可以在指定的时间间隔内执行某些任务。
在定时任务中,操作系统或应用程序会利用计时器或定时器来定期检查当前时间是否达到了预定的执行时间,如果当前时间已经达到了预定的时间,系统会自动执行相应的任务。在操作系统中,常见的定时任务管理工具有crontab(Linux系统)、Windows Task Scheduler(Windows系统)等。
定时任务可以定时执行,是因为操作系统或应用程序利用定时器周期性地检查当前时间,一旦达到预定时间就会自动执行相应的任务。
定时任务的执行依赖于 操作系统/线程调度机制,具体实现方式如下:
1. 基于 Thread.sleep()
的轮询(不推荐)
while (true) { Thread.sleep(5000); // 每 5s 执行一次 System.out.println("Task executed at: " + new Date()); }
缺点:阻塞线程,效率低。
2. 基于 ScheduledExecutorService
的任务队列
-
内部维护一个
DelayQueue
(延迟队列),存放待执行的任务。 -
线程池不断检查队列,取出到期的任务执行。
3. 操作系统定时器(如 Linux cron
)
-
对于
Quartz
或Spring Task
,底层可能依赖操作系统的定时任务机制(如Cron
)。
三、如何选择合适的定时任务方案?
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Timer | 简单单线程任务 | 使用简单 | 单线程,任务阻塞 |
ScheduledExecutorService | 多任务并发调度 | 线程池管理,稳定 | 需要手动管理线程池 |
Spring Task | Spring Boot 项目 | 注解驱动,支持 Cron | 不适合分布式场景 |
Quartz//xxl-job | 分布式、复杂任务调度 | 支持持久化、集群 | 配置复杂 |
@Scheduled + Cron | 固定时间任务(如每天凌晨执行) | 灵活,易维护 | 依赖 Spring 环境 |
四、最佳实践
-
单机简单任务 →
ScheduledExecutorService
或Spring Task
。 -
分布式任务 →
Quartz
(如订单超时检查、定时对账)。
还可以用作定时任务扫描
-
精确时间任务 →
Cron
表达式(如每天凌晨 3 点执行)。 -
避免
Timer
→ 单线程问题可能导致任务堆积。
总结
-
Java 原生:
Timer
(不推荐) →ScheduledExecutorService
(推荐)。 -
Spring 生态:
@Scheduled
+Cron
(简单) →Quartz
(复杂)。 -
底层原理:依赖线程调度或操作系统定时器(如
Cron
)。