很多开发者在第一次接触 Quartz 时,都会下意识觉得:
"为了准时触发任务,它一定在后台轮询时间吧?"
答案恰恰相反。Quartz 把"等待"这件事做到了极致------在 99% 的时间里,它的调度线程连一次空转都不会发生,而是安静地睡在操作系统的条件变量上,直到下一个任务真正需要被唤醒的那一刻。
下面,让我们把一次任务从"存入"到"触发"的全过程拆成七个阶段,彻底揭开 Quartz 的准时触发之谜。
一、启动:一条永不下班的"调度线程"
当代码执行
java
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
scheduler.start();
时,Quartz 在 JVM 里创建了一条名为 QuartzSchedulerThread
的线程。
这条线程只做一件事:
找到"下一个应该触发的 Trigger",然后 wait 到那个精确时间点。
二、登记:把触发器塞进"时间最小堆"
调用 scheduler.scheduleJob(job, trigger)
后,Trigger 被持久化到 JobStore。
- RAMJobStore :用一个优先级最小堆(
java.util.TreeSet<TriggerWrapper>
)保存所有 Trigger。 - JDBCJobStore :把触发器写进表
QRTZ_TRIGGERS
,再用 SQL 排序取出最早的一条。
无论哪种实现,堆顶永远是 最近要响铃的那一只。
三、计算下一次醒来的时间:idleWaitTime
QuartzSchedulerThread
每轮循环都会问 JobStore:
"在 now + idleWaitTime
之前,有没有需要触发的 Trigger?"
- 如果堆顶 Trigger 的
fireTime = T
,则
idleWaitTime = T - now
。 - 如果堆为空,则使用默认值(通常 30 秒)。
这个 idleWaitTime
就是接下来要沉睡的毫秒数。
四、真正的"睡眠":Object.wait(idleWaitTime)
线程执行:
java
synchronized (sigLock) {
sigLock.wait(idleWaitTime);
}
- 这段时间 CPU 占用率 = 0。
- 只有两种可能唤醒:
- 时钟到达
fireTime
,wait
自然返回; - 用户调用了
scheduleJob()
、pause()
、shutdown()
等操作,其他线程执行sigLock.notify()
提前唤醒。
- 时钟到达
五、抢锁:确保集群里只触发一次
如果部署了集群,多个节点会同时醒来。
- JDBCJobStore :利用
SELECT ... FOR UPDATE
对行加悲观锁,
把状态从WAITING
改为ACQUIRED
,只有一个节点能成功。 - RAMJobStore :在内存里加
synchronized
,单机场景无竞争。
六、执行:把 Trigger 变成真正的任务线程
QuartzSchedulerThread
把 Trigger 封装成TriggerFiredBundle
。- 丢进线程池
SimpleThreadPool
,由WorkerThread
执行:- 反射创建 Job 实例
- 调用
job.execute(context)
- 触发监听器
- 更新 Trigger 的下次触发时间
- 把更新后的 Trigger 重新放回 JobStore 的堆中
七、回到步骤三:循环永不休止
新一轮循环开始,堆顶又换成了下一次要触发的 Trigger,
idleWaitTime
被重新计算,线程再次进入 wait
------
周而复始,精准而省电。
时间线图解(单机 RAMJobStore)
less
T0 用户调用 scheduleJob(triggerA@T3)
├─ heap = [triggerA@T3]
T1 QuartzSchedulerThread
├─ idleWaitTime = 2s
└─ sigLock.wait(2s) // CPU 0%
T3 wait 返回
├─ acquireNextTriggers → triggerA
├─ WorkerThread 执行 JobA
└─ heap = [triggerB@T5]
T3+ε idleWaitTime = 2s
└─ sigLock.wait(2s) // 再次沉睡
八、Quartz Scheduler 通过 Job
和 Trigger
来管理任务及其执行时间
Quartz 的定时任务实现主要基于几个核心组件:调度器(Scheduler)、触发器(Trigger)、作业(Job) ,这三者共同工作实现了精准的任务调度。调度器作为任务的管理中心,触发器定义任务执行的时间规则,而作业则是具体要执行的任务内容。特别是触发器,它是定时任务实现的关键,通过设定触发器中的时间策略,如简单触发器(SimpleTrigger)和Cron触发器(CronTrigger),Quartz可按照精确的时间计划执行任务。
在使用 Quartz Scheduler 时,任务的执行时间是由你通过
Trigger
来定义的。Quartz 支持两种主要类型的触发器:SimpleTrigger
和 CronTrigger
。这两种触发器允许你以不同的方式指定任务的执行时间。
SimpleTrigger
SimpleTrigger
适用于简单的调度需求,比如每隔一段时间执行一次任务,或者在某个具体的时间点执行一次任务。
示例:使用 SimpleTrigger 在特定时间执行任务
java
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(ScheduledTask.class)
.withIdentity("scheduled-task")
.storeDurably()
.build();
}
@Bean
public Trigger trigger(JobDetail jobDetail) {
// 设置任务在2025年8月1日午夜执行一次
LocalDateTime executionTime = LocalDateTime.of(2025, 8, 1, 0, 0);
Date date = Date.from(executionTime.atZone(ZoneId.systemDefault()).toInstant());
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity("simple-trigger-for-scheduled-task")
.startAt(date)
.build();
}
}
CronTrigger
CronTrigger
使用 cron 表达式来定义复杂的调度模式。cron 表达式可以精确地指定任务的执行时间,包括秒、分钟、小时、日期、月份和星期几等。
示例:使用 CronTrigger 每天凌晨两点执行任务
java
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(ScheduledTask.class)
.withIdentity("scheduled-task")
.storeDurably()
.build();
}
@Bean
public Trigger trigger(JobDetail jobDetail) {
// 每天凌晨2点执行
String cronExpression = "0 0 2 * * ?";
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity("cron-trigger-for-scheduled-task")
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build();
}
}
监控任务执行
为了确保任务按照预期的时间执行,你可以使用 Quartz 提供的监听器来监控任务的状态。
示例:使用 JobListener 监听任务执行
java
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.springframework.stereotype.Component;
@Component
public class MyJobListener implements JobListener {
@Override
public String getName() {
return "my-job-listener";
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("Job is about to be executed: " + context.getJobDetail().getKey());
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println("Job execution vetoed: " + context.getJobDetail().getKey());
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println("Job was executed: " + context.getJobDetail().getKey());
if (jobException != null) {
System.err.println("Error occurred while executing job: " + jobException.getMessage());
}
}
}
注册 Listener
java
import org.quartz.Scheduler;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
@Autowired
private MyJobListener myJobListener;
@Bean
public Scheduler scheduler() throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 添加 JobListener 到 Scheduler
scheduler.getListenerManager().addJobListener(myJobListener, KeyMatcher.keyEquals(jobDetail().getKey()));
return scheduler;
}
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(ScheduledTask.class)
.withIdentity("scheduled-task")
.storeDurably()
.build();
}
@Bean
public Trigger trigger(JobDetail jobDetail) {
// 每天凌晨2点执行
String cronExpression = "0 0 2 * * ?";
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity("cron-trigger-for-scheduled-task")
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build();
}
}
常见疑问解答
-
会不会轮询?
不会。调度线程没有
sleep(1000)
这种忙等,而是wait
直到下一次触发时刻。 -
系统时间回拨怎么办?
Quartz 的
MisfireHandler
会检测漏掉的触发器并补偿;生产环境应配置 NTP 或开启
org.quartz.scheduler.skipUpdateCheck=true
减少误差。 -
JVM 崩溃后任务会丢吗?
使用 JDBCJobStore 时,Trigger 状态持久化在数据库,重启后
MisfireHandler
会恢复执行。 -
为什么偶尔看到 1% CPU?
那是
MisfireHandler
、ClusterManager
等辅助线程的心跳,主调度线程仍然处于wait
状态。
结语
Quartz 把"何时醒来"本身抽象成 堆顶 Trigger 的 fireTime ,
把操作系统的条件变量当成闹钟,
于是它既能在毫秒级准时触发,又几乎不浪费一丝 CPU。
理解这一机制后,再去看 Quartz 的源码、调优参数或排障日志,
你会发现:调度器的心跳,其实是一条优雅的睡眠线。