当前位置: 首页 > news >正文

Spring Boot定时任务原理

Spring Boot定时任务原理

在现代应用中,定时任务的调度是实现周期性操作的关键机制。Spring Boot 提供了强大的定时任务支持,通过注解驱动的方式,开发者可以轻松地为方法添加定时任务功能。本文将深入探讨 Spring Boot 中定时任务的实现原理,重点分析 @EnableSchedulingScheduledAnnotationBeanPostProcessor 的作用,以及任务如何被注册和执行。我们还将详细介绍底层使用的线程池调度器 ThreadPoolTaskScheduler 和 Java 内置的 ScheduledThreadPoolExecutor,它们如何协同工作,保证定时任务的准确执行。此外,我们还将探讨任务调度的线程阻塞与唤醒机制,深入剖析延迟队列(DelayedWorkQueue)如何有效管理任务的执行顺序。通过本文的学习,你将能够更好地理解和应用 Spring Boot 定时任务,提升应用的调度能力和性能。

1.注解驱动

Spring Boot通过@EnableScheduling激活定时任务支持,而EnableScheduling注解导入了SchedulingConfiguration,这个类创建了一个名为ScheduledAnnotationBeanPostProcessorbean,而这个bean就是定时任务的关键

/*** {@code @Configuration} class that registers a {@link ScheduledAnnotationBeanPostProcessor}* bean capable of processing Spring's @{@link Scheduled} annotation.** <p>This configuration class is automatically imported when using the* {@link EnableScheduling @EnableScheduling} annotation. See* {@code @EnableScheduling}'s javadoc for complete usage details.** @author Chris Beams* @since 3.1* @see EnableScheduling* @see ScheduledAnnotationBeanPostProcessor*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {return new ScheduledAnnotationBeanPostProcessor();}}

2.对ScheduledAnnotationBeanPostProcessor的分析

1. 类职责

  • 核心作用:扫描 Spring Bean 中的 @Scheduled 注解方法,将其转换为定时任务,并注册到任务调度器。

2. 定时任务注册的关键流程

代码都是经过简化的代码,实际上我去看Spring的源码,发现代码都很长,但是整体意思是差不多的

Bean 初始化后扫描注解(关键方法:postProcessAfterInitialization
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {// 1. 跳过 AOP 基础设施类if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||bean instanceof ScheduledExecutorService) {// Ignore AOP infrastructure such as scoped proxies.return bean;}// 2. 检查类是否包含 @Scheduled 注解Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);if (!nonAnnotatedClasses.contains(targetClass) && AnnotationUtils.isCandidateClass(targetClass, List.of(Scheduled.class, Schedules.class))) {// 3. 反射查找所有带 @Scheduled 的方法Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, method -> AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class));// 4. 处理每个带注解的方法annotatedMethods.forEach((method, scheduledAnnotations) -> scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));}return bean;
}
  • 跳过无关 Bean:如 AOP 代理类、TaskScheduler 本身。
  • 反射扫描方法:通过 MethodIntrospector 查找所有带有 @Scheduled 的方法。
  • 注解聚合:支持 @Schedules 多注解合并。
解析任务参数并注册(关键方法:processScheduled
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {// 1. 创建 Runnable 任务Runnable runnable = createRunnable(bean, method);// 2. 解析时间参数(cron/fixedDelay/fixedRate)if (StringUtils.hasText(cron)) {// 处理 cron 表达式CronTask task = new CronTask(runnable, new CronTrigger(cron, timeZone));tasks.add(registrar.scheduleCronTask(task));} else if (fixedDelay > 0) {// 处理 fixedDelayFixedDelayTask task = new FixedDelayTask(runnable, fixedDelay, initialDelay);tasks.add(registrar.scheduleFixedDelayTask(task));} else if (fixedRate > 0) {// 处理 fixedRateFixedRateTask task = new FixedRateTask(runnable, fixedRate, initialDelay);tasks.add(registrar.scheduleFixedRateTask(task));}// 3. 注册任务到 ScheduledTaskRegistrarsynchronized (scheduledTasks) {scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>()).addAll(tasks);}
}
  • 任务封装:将方法封装为 ScheduledMethodRunnable
  • 时间参数解析:
    • 支持 cronfixedDelayfixedRate 三种模式。
    • 处理 initialDelay 初始延迟。
    • 使用 embeddedValueResolver 解析占位符(如 ${task.interval})。
  • 任务注册:最终任务被添加到 ScheduledTaskRegistrar
启动任务调度(关键方法:finishRegistration
private void finishRegistration() {// 1. 配置 TaskScheduler(优先级:显式设置 > 查找 Bean > 默认单线程)if (registrar.getScheduler() == null) {TaskScheduler scheduler = resolveSchedulerBean(beanFactory, TaskScheduler.class, false);registrar.setTaskScheduler(scheduler);}// 2. 调用 SchedulingConfigurer 自定义配置(扩展点)List<SchedulingConfigurer> configurers = beanFactory.getBeansOfType(SchedulingConfigurer.class);configurers.forEach(configurer -> configurer.configureTasks(registrar));// 3. 启动所有注册的任务registrar.afterPropertiesSet();
}
  • 调度器解析:
    • 默认查找名为 taskScheduler 的 Bean。
    • 若无则创建单线程调度器(Executors.newSingleThreadScheduledExecutor())。
  • 扩展点:允许通过 SchedulingConfigurer 自定义任务注册逻辑。
  • 最终启动:调用 afterPropertiesSet() 触发任务调度。

3.ThreadPoolTaskScheduler的剖析

ThreadPoolTaskScheduler 是 Spring 对 Java ScheduledThreadPoolExecutor 的封装,是 @Scheduled 定时任务的底层执行引擎。

  • 继承关系:继承 ExecutorConfigurationSupport,实现 TaskScheduler 接口,整合了线程池管理与定时任务调度。
  • 底层依赖:基于 ScheduledThreadPoolExecutor,支持 周期性任务(fixedRate/fixedDelay)和 动态触发任务(如 cron 表达式)。

线程池初始化(关键方法:initializeExecutor

同样,这里和以后的部分也都是伪代码

@Override
protected ExecutorService initializeExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {// 创建 ScheduledThreadPoolExecutorthis.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);// 配置线程池策略(如取消后立即移除任务)if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor scheduledPoolExecutor) {scheduledPoolExecutor.setRemoveOnCancelPolicy(this.removeOnCancelPolicy);// 其他策略设置...}return this.scheduledExecutor;
}

这部分是我复制源码的,可以清晰的看到,底层就是new了ScheduledThreadPoolExecutor

	protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler);}

4.ScheduledThreadPoolExecutor的原理分析

核心成员:

  • 任务队列:使用 DelayedWorkQueue(内部实现的小顶堆),按任务执行时间排序。
  • 线程池:复用 ThreadPoolExecutor 的线程管理机制,支持核心线程数和最大线程数配置。

2. 定时任务调度机制

所有定时任务被封装为 ScheduledFutureTask 对象,其核心逻辑如下:

private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {private long time;          // 下一次执行时间(纳秒)private final long period;  // 周期(正数:fixedRate;负数:fixedDelay)private int heapIndex;      // 在 DelayedWorkQueue 中的索引public void run() {if (isPeriodic()) {// 周期性任务:重新计算下一次执行时间,并重新加入队列setNextRunTime();reExecutePeriodic(outerTask);} else {// 一次性任务:直接执行super.run();}}
}
  1. 任务提交:通过 schedulescheduleAtFixedRate 等方法提交任务。
  2. 队列管理:任务被封装为 ScheduledFutureTask 并加入 DelayedWorkQueue
  3. 线程唤醒:工作线程 (Worker) 从队列获取任务,若任务未到执行时间,线程进入限时等待(available.awaitNanos(delay))。
  4. 任务执行:到达执行时间后,线程执行任务:
    • 固定速率(fixedRate):执行完成后,根据 period 计算下一次执行时间(time += period)。
    • 固定延迟(fixedDelay):执行完成后,根据当前时间计算下一次执行时间(time = now() + (-period))。
  5. 重新入队:周期性任务执行后,重新加入队列等待下次调度。

3.DelayedWorkQueue的简单剖析

DelayQueue队列是一个延迟队列,DelayQueue中存放的元素必须实现Delayed接口的元素,实现接口后相当于是每个元素都有个过期时间,当队列进行take获取元素时,先要判断元素有没有过期,只有过期的元素才能出队操作,没有过期的队列需要等待剩余过期时间才能进行出队操作。

DelayQueue队列内部使用了PriorityQueue优先队列来进行存放数据,它采用的是二叉堆进行的优先队列,使用ReentrantLock锁来控制线程同步,由于内部元素是采用的PriorityQueue来进行存放数据,所以Delayed接口实现了Comparable接口,用于比较来控制优先级

线程阻塞与唤醒逻辑
(1) 取任务时的阻塞(take() 方法)

当线程调用 take() 方法从队列中获取任务时,若队列为空或队头任务未到期,线程会进入阻塞状态:

public E take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {for (;;) {E first = q.peek();if (first == null) {available.await(); // 队列为空时无限等待} else {long delay = first.getDelay(NANOSECONDS);if (delay <= 0) return q.poll(); // 任务已到期,取出执行if (leader != null) {available.await(); // 其他线程已为队头任务等待,本线程无限等待} else {Thread thisThread = Thread.currentThread();leader = thisThread; // 标记当前线程为“领导者”try {available.awaitNanos(delay); // 限时等待到期时间} finally {if (leader == thisThread) leader = null;}}}}} finally {if (leader == null && q.peek() != null) available.signal();lock.unlock();}
}
  • 关键逻辑:
    • leader 线程优化:避免多个线程同时等待同一任务到期,仅一个线程(leader)限时等待,其他线程无限等待
    • 限时等待:通过 available.awaitNanos(delay) 阻塞到任务到期时间。
(2) 插入新任务时的唤醒(offer() 方法)

当新任务被插入队列时,若新任务成为队头(即最早到期),会触发唤醒逻辑:

public boolean offer(E e) {final ReentrantLock lock = this.lock;lock.lock();try {q.offer(e); // 插入任务并调整堆结构if (q.peek() == e) { // 新任务成为队头leader = null;available.signal(); // 唤醒等待线程}return true;} finally {lock.unlock();}
}
  • 唤醒条件:
    • 插入的任务成为新的队头(即其到期时间最早)。
    • 调用available.signal()唤醒等待的线程(leader)或其他线程

(3) 唤醒机制总结
  • 何时唤醒:
    1. 超时唤醒:等待线程因任务到期而被 JVM 自动唤醒。
    2. 插入新任务唤醒:新任务的到期时间早于当前队头任务时,插入线程会触发唤醒。
  • 唤醒对象:
    • 若存在 leader 线程(正在限时等待队头任务),优先唤醒它。
    • 若无 leader,唤醒任意一个等待线程

相关文章:

Spring Boot定时任务原理

Spring Boot定时任务原理 在现代应用中&#xff0c;定时任务的调度是实现周期性操作的关键机制。Spring Boot 提供了强大的定时任务支持&#xff0c;通过注解驱动的方式&#xff0c;开发者可以轻松地为方法添加定时任务功能。本文将深入探讨 Spring Boot 中定时任务的实现原理…...

C#初级教程(7)——初级期末检测

练习 1&#xff1a;计算圆的周长和面积 改编题目&#xff1a;编写一个 C# 程序&#xff0c;让用户输入圆的半径&#xff0c;然后计算并输出该圆的周长和面积&#xff0c;结果保留两位小数。 using System;class CircleCalculation {static void Main(){const double pi 3.14…...

原生稀疏注意力机制(NSA):硬件对齐且可原生训练的稀疏注意力机制-论文阅读

摘要 长上下文建模对于下一代语言模型至关重要&#xff0c;但标准注意力机制的高计算成本带来了巨大的计算挑战。稀疏注意力提供了一种在保持模型能力的同时提高效率的有前途的方向。本文提出了一种名为 NSA&#xff08;原生可训练稀疏注意力机制&#xff09; 的方法&#xff…...

Apache Struts RCE (CVE-2024-53677)

前言 对目前的Apache Struts RCE (CVE-2024-53677)的poc进行总结&#xff0c;由于只能单个ip验证&#xff0c;所以自己更改一下代码&#xff0c;实现&#xff1a;多线程读取url验证并保存&#xff0c;更改为中文解释 免责声明 请勿利用文章内的相关技术从事非法测试&#xf…...

GIS地图、轨道交通与智能驾驶UI设计:未来交通的智能化探索

随着科技的飞速发展&#xff0c;我们正迎来一个高度智能化的未来。在这个时代背景下&#xff0c;GIS&#xff08;地理信息系统&#xff09;、轨道交通以及智能驾驶UI设计正逐步成为推动交通行业变革的重要力量。本文将深入探讨这三者之间的内在联系及其在未来交通系统中的应用前…...

OpenResty

文章目录 OpenResty执行原理getting-started 核心模块: lua-nginx-module (ngx_lua)常用指令配置指令的执行顺序 API OpenResty 官方文档: http://openresty.org/ 官方文档完全不明所以, 除了getting-started完全不知道下一步该干啥 (都不知道ngx是什么它就开始用了), 找不到架…...

如何将公钥正确添加到服务器的 authorized_keys 文件中以实现免密码 SSH 登录

1. 下载密钥文件 2. RSA 解析 将 id_ed25519 类型的私钥转换为 RSA 类型&#xff0c;要将 ED25519 私钥转换为 RSA 私钥&#xff0c;需要重新生成一个新的 RSA 密钥对。 步骤&#xff1a; 生成新的 RSA 密钥对 使用 ssh-keygen 来生成一个新的 RSA 密钥对。比如&#xff0c;执…...

SQLMesh 系列教程7- 详解 seed 模型

SQLMesh 是一个强大的数据建模和管道管理工具&#xff0c;允许用户通过 SQL 语句定义数据模型并进行版本控制。Seed 模型是 SQLMesh 中的一种特殊模型&#xff0c;主要用于初始化和填充基础数据集。它通常包含静态数据&#xff0c;如参考数据和配置数据&#xff0c;旨在为后续的…...

Git常见命令--助力开发

git常见命令&#xff1a; 创建初始化仓库&#xff1a; git 将文件提交到暂存区 git add 文件名 将文件提交到工作区 git commit -m "注释&#xff08;例如这是发行的版本1&#xff09;" 文件名 查看状态 如果暂存区没有文件被提交显示&#xff1a; $ git status On…...

学习整理安装php的uuid扩展以及uuid调用方法

学习整理安装php的uuid扩展以及uuid调用方法 1、安装uuid依赖库2、下载并安装3、ini中添加扩展4、re2c版本报错5、uuid调用方法 1、安装uuid依赖库 yum -y install uuid uuid-devel e2fsprogs-devel libuuid-devel2、下载并安装 点我下载uuid安装包 wget http://pecl.php.ne…...

算法系列之贪心算法

在算法中&#xff0c;贪心算法&#xff08;Greedy Algorithm&#xff09;是一种常见的解决优化问题的算法。贪心算法的核心思想是&#xff1a;在每一步选择中都采取当前状态下最优的选择&#xff0c;即贪心的做出局部最优的决策&#xff0c;从而希望最终能够得到全局最优解。尽…...

将产品照片(form.productPhotos)转为 JSON 字符串发送给后端

文章目录 1. 前端 form.productPhotos 的当前处理a. 组件绑定b. 当前发送逻辑 2. 如何将 form.productPhotos 转为 JSON 字符串发送给后端a. 修改前端 save() 方法b. 确保 esave API 支持接收字符串 基于你提供的 identify-form.vue 代码&#xff0c;我将分析如何将产品照片&a…...

『大模型笔记』详细对比GraphRAG与传统RAG!

详细对比GraphRAG与传统RAG! 文章目录 详细对比GraphRAG与传统RAG!要点最终内容1. GraphRAG的作用与应用场景2. GraphRAG与传统RAG的对比3. GraphRAG的工作原理4. GraphRAG如何提高准确性和提供完整答案5. GraphRAG在开发和维护中的优势6. GraphRAG对生产环境的影响7. GraphR…...

安全面试3

文章目录 一个单位的一级域名可能不止一个&#xff0c;怎么收集某个单位的所有域名&#xff0c;注意不是子域名用转义字符防御时&#xff0c;如果遇到数据库的列名或是表名本身就带着特殊字符&#xff0c;应该怎么做宽字节注入原理防御宽字节注入的方法 基于黑白名单的修复&…...

软件测试:1、单元测试

1. 单元测试的基本概念 单元&#xff08;Unit&#xff09;&#xff1a;软件系统的基本组成单位&#xff0c;可以是函数、模块、方法或类。 单元测试&#xff08;Unit Testing&#xff09;&#xff1a;对软件单元进行的测试&#xff0c;验证代码的正确性、规范性、安全性和性能…...

球队训练信息管理系统设计与实现(代码+数据库+LW)

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装球队训练信息管理系统软件来发挥其高效地信息处理的作用&a…...

【Bluedroid】AVRCP 连接源码分析(二)

接着上一篇【Bluedroid】AVRCP 连接源码分析(一)-CSDN博客,继续AVRCP连接的源码分析。 getcapabilities_cmd packages/modules/Bluetooth/system/btif/src/btif_rc.cc /***************************************************************************** Function …...

OSS(对象存储服务)

OSS&#xff08;对象存储服务&#xff09; 是一种用于存储和管理非结构化数据的云存储服务&#xff0c;其核心设计面向海量数据的高扩展性、高可靠性和低成本存储。以下从定义、核心原理、架构特点和应用场景等方面详细介绍&#xff1a; 一、什么是OSS&#xff1f; OSS&#x…...

《深入理解JVM》实战笔记(二): 类加载机制与类加载器

序言 Java 语言的强大之处之一在于其动态加载的能力&#xff0c;使得 Java 程序可以在运行时加载新的类&#xff0c;而不需要在编译时确定所有的类信息。这一切都离不开 JVM 的类加载机制。本篇博客将详细探讨 JVM 的类加载过程以及类加载器的工作原理&#xff0c;帮助你更深入…...

ChromeDriver下载

平时为了下个驱动&#xff0c;到处找挺麻烦&#xff0c;收集了很多无偿分享给需要的人&#xff0c;仅供学习和交流。 ChromeDriver 102.0.5005.61 ChromeDriver 105.0.5195.102 ChromeDriver 108.0.5359.71 ChromeDriver 111.0.5563.64 ChromeDriver 116.0.5845.97 Chrom…...

从‘Hello World’到区块链:手把手用Python代码演示SHA哈希的十大实战应用

从‘Hello World’到区块链&#xff1a;手把手用Python代码演示SHA哈希的十大实战应用 哈希函数就像数字世界的指纹采集器——它能将任意长度的数据压缩成固定长度的唯一标识。想象一下&#xff0c;你只需要对比两个文件的指纹就能判断它们是否完全相同&#xff0c;而无需逐字节…...

Vivado 中 Xilinx DDR4 MIG 的实战配置与性能调优

1. DDR4 MIG IP核基础配置 在Vivado中配置DDR4 MIG&#xff08;Memory Interface Generator&#xff09;IP核是搭建高速存储系统的第一步。我最近在一个数据采集项目中就遇到了这个需求&#xff0c;当时需要处理每秒超过5GB的传感器数据流。下面分享我的实战经验&#xff0c;帮…...

用51单片机+DAC0832做个简易信号发生器:手把手教你生成方波、三角波和锯齿波(附完整汇编代码)

51单片机与DAC0832实战&#xff1a;三波形信号发生器的设计与实现 在电子工程和嵌入式系统开发中&#xff0c;信号发生器是一个基础但极其重要的工具。无论是用于电路测试、教学演示还是原型验证&#xff0c;一个可靠的信号源都能大大提升工作效率。本文将带你从零开始&#xf…...

**Shader优化实战:从冗余计算到性能跃升的极致之旅**在图形渲染领域,**Shader性能优化**早已不是锦上添花的技术

Shader优化实战&#xff1a;从冗余计算到性能跃升的极致之旅 在图形渲染领域&#xff0c;Shader性能优化早已不是锦上添花的技术点&#xff0c;而是决定项目成败的核心环节。尤其是在移动端、VR/AR或高帧率游戏开发中&#xff0c;一个低效的着色器可能直接导致掉帧、发热甚至崩…...

倾斜光栅的参数优化及公差分析

摘要 对于背光系统、光内连器和近眼显示器等许多应用来说&#xff0c;将光高效地耦合到引导结构中是一个重要的问题。对于这种应用&#xff0c;倾斜光栅以能够高效地耦合单色光而闻名。在本例中&#xff0c;提出了利用严格傅里叶模态方法&#xff08;FMM&#xff0c;也称为RCWA…...

Docker 27车载部署必须关闭的5个默认守护进程,否则QNX/Hypervisor共存环境下将触发TSN时间同步漂移

第一章&#xff1a;Docker 27车载部署的TSN时间同步风险总览在车载边缘计算场景中&#xff0c;Docker 27&#xff08;即 Docker v27.x 系列&#xff09;被广泛用于容器化部署时间敏感网络&#xff08;TSN&#xff09;相关的实时通信组件&#xff0c;例如 IEEE 802.1AS-2020 时间…...

告别点灯:用STM32CubeIDE和HAL库,给你的SSD1306 OLED做个动态仪表盘

用STM32CubeIDE和HAL库打造SSD1306 OLED动态仪表盘 在嵌入式开发中&#xff0c;OLED屏幕因其高对比度、低功耗和快速响应等特性&#xff0c;成为数据显示的理想选择。本文将带你从零开始&#xff0c;使用STM32CubeIDE和HAL库&#xff0c;为SSD1306 OLED屏幕开发一个功能丰富的动…...

手把手教你用QT QSlider做一个音量调节控件(附完整信号槽连接代码)

实战指南&#xff1a;用QSlider打造专业级音量控制组件 在桌面应用开发中&#xff0c;音量调节控件是最常见但最容易被忽视的交互元素之一。一个优秀的音量滑块不仅需要精确控制音频输出&#xff0c;还要符合用户的操作直觉——无论是拖动滑块还是点击滑条区域&#xff0c;都应…...

Navicat连ClickHouse出现中文乱码怎么办_字符集编码调整

Navicat连ClickHouse中文显示问号或方块的根本原因是连接未显式声明UTF-8编码&#xff0c;需在连接字符串中添加?charsetUTF-8&#xff08;JDBC&#xff09;或&charsetUTF-8&#xff08;ODBC/原生&#xff09;&#xff0c;并确保驱动版本支持&#xff08;clickhouse-jdbc …...

Spring Boot项目里MySQL连接突然断开的排查与修复(附HikariCP配置)

Spring Boot项目中MySQL连接断开的深度排查与HikariCP优化实战 凌晨三点&#xff0c;监控系统突然发出刺耳的警报声——你的Spring Boot应用在夜间低峰期出现了大量"Communications link failure"错误。这不是第一次了&#xff0c;每次都是夜深人静时发生&#xff0c…...