深入 Quartz 的内核:它是如何“到点就把任务叫醒”的

很多开发者在第一次接触 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。
  • 只有两种可能唤醒:
    1. 时钟到达 fireTimewait 自然返回;
    2. 用户调用了 scheduleJob()pause()shutdown() 等操作,其他线程执行 sigLock.notify() 提前唤醒。

五、抢锁:确保集群里只触发一次

如果部署了集群,多个节点会同时醒来。

  • JDBCJobStore :利用 SELECT ... FOR UPDATE 对行加悲观锁,
    把状态从 WAITING 改为 ACQUIRED,只有一个节点能成功。
  • RAMJobStore :在内存里加 synchronized,单机场景无竞争。

六、执行:把 Trigger 变成真正的任务线程

  1. QuartzSchedulerThread 把 Trigger 封装成 TriggerFiredBundle
  2. 丢进线程池 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 通过 JobTrigger 来管理任务及其执行时间

Quartz 的定时任务实现主要基于几个核心组件:调度器(Scheduler)、触发器(Trigger)、作业(Job) ,这三者共同工作实现了精准的任务调度。调度器作为任务的管理中心,触发器定义任务执行的时间规则,而作业则是具体要执行的任务内容。特别是触发器,它是定时任务实现的关键,通过设定触发器中的时间策略,如简单触发器(SimpleTrigger)和Cron触发器(CronTrigger),Quartz可按照精确的时间计划执行任务。

在使用 Quartz Scheduler 时,任务的执行时间是由你通过 Trigger 来定义的。Quartz 支持两种主要类型的触发器:SimpleTriggerCronTrigger。这两种触发器允许你以不同的方式指定任务的执行时间。

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();
    }
}

常见疑问解答

  1. 会不会轮询?

    不会。调度线程没有 sleep(1000) 这种忙等,而是 wait 直到下一次触发时刻。

  2. 系统时间回拨怎么办?

    Quartz 的 MisfireHandler 会检测漏掉的触发器并补偿;

    生产环境应配置 NTP 或开启 org.quartz.scheduler.skipUpdateCheck=true 减少误差。

  3. JVM 崩溃后任务会丢吗?

    使用 JDBCJobStore 时,Trigger 状态持久化在数据库,重启后 MisfireHandler 会恢复执行。

  4. 为什么偶尔看到 1% CPU?

    那是 MisfireHandlerClusterManager 等辅助线程的心跳,主调度线程仍然处于 wait 状态。


结语

Quartz 把"何时醒来"本身抽象成 堆顶 Trigger 的 fireTime

把操作系统的条件变量当成闹钟,

于是它既能在毫秒级准时触发,又几乎不浪费一丝 CPU。

理解这一机制后,再去看 Quartz 的源码、调优参数或排障日志,

你会发现:调度器的心跳,其实是一条优雅的睡眠线。

相关推荐
weixin_491853313 分钟前
Spring Boot 中整合WebSocket
spring boot·后端·websocket
超浪的晨6 分钟前
Maven 与单元测试:JavaWeb 项目质量保障的基石
java·开发语言·学习·单元测试·maven·个人开发
不会理财的程序员不是好老板10 分钟前
Java Spring Boot项目中集成Swagger完整步骤
java·开发语言·spring boot
charlie11451419115 分钟前
计算机网络八股文——TCP,UDP
网络·网络协议·tcp/ip·计算机网络·面试·udp·八股文
Kookoos19 分钟前
ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层
后端·微服务·.net·graphql·abp vnext·schema 分层
不太厉害的程序员20 分钟前
eclipse更改jdk环境和生成webservice客户端代码
java·ide·后端·eclipse·webservice
悟能不能悟21 分钟前
ode with me是idea中用来干嘛的插件
java·ide·intellij-idea
Murphy_lx21 分钟前
C++多态的原理
java·开发语言·c++
神仙别闹24 分钟前
基于JSP+MySQL 实现(Web)毕业设计题目收集系统
java·前端·mysql
PineappleCoder29 分钟前
用 “餐厅模型” 吃透事件循环:同步、宏任务、微任务谁先执行?
前端·javascript·面试