Java定时任务调度原理解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 随着互联网应用的快速普及,开发者们往往会遇到业务逻辑复杂、时间驱动类型业务、数据处理、离线分析等场景,比如整点发送优惠券、按月批量统计报表等,为了减少对核心系统的影响,我们通常会采用定时任务框架来处理。定时任务顾名思义就是预先设定任务执行时间,到点后任务自动被调度执行,下面列出几种常见的定时任务框架并简单介绍其实现原理。

1,定时任务管理简介

随着互联网应用的快速普及,开发者们往往会遇到业务逻辑复杂、时间驱动类型业务、数据处理、离线分析等场景,比如整点发送优惠券、按月批量统计报表等,为了减少对核心系统的影响,我们通常会采用定时任务框架来处理。定时任务顾名思义就是预先设定任务执行时间,到点后任务自动被调度执行,下面列出几种常见的定时任务框架并简单介绍其实现原理。

2 Java 原生定时任务调度器

2.1 Timer

2.1.1 简介

Timer是从Java SDK1.3开始提供的最原生定时任务执行解决方案,位于java.util包下,主要包括如下四类角色:

  • Timer:任务调度器
  • TimerThread:任务执行器
  • TimerTask:定时任务
  • TaskQueue:任务队列,队列中的任务按执行时间先后顺序排序,队首执行时间最靠前

2.1.2 关键源码解析

Timer的实现非常简单,翻看源代码我们来看下它的核心调度和执行处理过程:

privatevoidmainLoop() {
while (true) {
try {
TimerTasktask;
booleantaskFired;
synchronized(queue) {  // 同步锁住队列                 .....
task=queue.getMin();  // 取出队首任务 synchronized(task.lock)  { 
                    .....
if (taskFired= (executionTime<=currentTime)) {  // 如果达到执行时间设置可执行标志if (task.period==0) { 
queue.removeMin();   // 移除队列task.state=TimerTask.EXECUTED;   // 标记完成状态                        } else {
queue.rescheduleMin(
task.period<0?currentTime-task.period : executionTime+task.period);
                        }
                    }
                }
if (!taskFired) queue.wait(executionTime-currentTime); // 如果时间没达到,等待△time            }
if (taskFired)  // 如果可执行则runtask.run();
        } catch(InterruptedExceptione) {
        }
    }
}


借助同步悲观锁机制,整个执行器采用单线程无限遍历队列来实现,不断取出队首任务,判断是否达到执行时间,如果达到则执行,如果没有达到则等待直到达到执行时间。执行过程可简化为如下流程:

2.1.3 优缺点

优点:简单易用。

缺陷:不支持多线程;对系统时钟敏感;当前任务异常会终止队列中后续任务执行;不支持定时表达式。

2.2 ScheduledExecutorService

2.2.1 简介

我们知道Java1.5是Java历史版本上的一个重大转折点,天才并发大师Doug Lee为Java带来了完整的线程池

编程框架J.U.C,结束了Java只能手动创建线程的历史,使得多线程编程更加简单、安全和高效。我们先来看下J.U.C包下线程池的类图:

其中有两个可直接使用的实例化类,ThreadPoolExecutor和ScheduledThreadPoolExecutor,前者是最基础的通用线程池,使用者可以通过灵活的构造函数传参创建所需要的线程池。后者就是我们要介绍的定时任务管理器。ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说任务是并发执行,互不影响。需要注意的是只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是出于轮询任务的状态。

ScheduledExecutorService定时任务框架体系也拥有定时任务的4个主要角色:

  • ScheduledThreadPoolExecutor:调度器
  • ThreadPoolExecutor:执行器
  • ScheduledFutureTask:定时任务,记录执行时间和周期
  • DelayedWorkQueue:任务队列

2.2.2 关键源码解析

在深入了解Java并发包下的定时任务调度之前,强烈建议先认真阅读2.1中Timer的核心思路,因为ScheduledExecutorService实现定时任务调度的最本质思路和Timer基本如出一辙,最大的改变就是将Timer的单线程变成了多线程。

我们先来看下其实现类ScheduledThreadPoolExecutor的创建过程:

publicScheduledThreadPoolExecutor(intcorePoolSize,
ThreadFactorythreadFactory,
RejectedExecutionHandlerhandler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
newDelayedWorkQueue(), threadFactory, handler);
}

可以看出它直接调用了通用父类线程池ThreadPoolExecutor来实例化自己,也就说任务的执行是通过ThreadPoolExecutor来实现,另外值得关注的是任务队列它采用了延迟队列DelayedWorkQueue,DelayedWorkQueue是ScheduledThreadPoolExecutor的内部类。我们来看下DelayedWorkQueue对定时任务是如何管理的:

// 添加定时任务publicbooleanoffer(Runnablex) {
    ...
finalReentrantLocklock=this.lock;
lock.lock();
try {
inti=size;
if (i>=queue.length) grow();
size=i+1;
if (i==0) {  // 队列为空,则直接加到队首queue[0] =e;
setIndex(e, 0);
        } else {
siftUp(i, e);   // 队列不对空,调用siftUp方法决定x在队列中的顺序,shiftUp通过调用任务的compareTo来实现将定时执行时间最早的放在最前面        }
if (queue[0] ==e) {
leader=null;
available.signal();
        }
    } finally {
lock.unlock();
    }
returntrue;
}

上述的siftUp方法使得任务队列和Timer中的任务队列达到了同样的效果,定时执行时间最早的放在队列最前面,我们再来看下它是如何达成定时执行的。

在ScheduledThreadPoolExecutor中我们可以看到所有提交的定时任务最后都调用了ensurePrestart()方法,它是父类线程池的方法,调用了addWorker()方法,该方法是线程池对线程的核心管理方法,不过不是本文章的重点不细讲,ensurePrestart方法中调用addWorker方法传递的firstTask都是null,也就是说给线程池提交了一个空任务,那么线程池执行任务都需要从队列中拉取的任务,那我们来看下队列拿任务的take方法的实现:

publicRunnableScheduledFuture<?>take() throwsInterruptedException {
finalReentrantLocklock=this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?>first=queue[0]; // 直接获取队首任务if (first==null)
available.await();
else {
longdelay=first.getDelay(NANOSECONDS);  // 队首任务执行时间和当前时间的时间差if (delay<=0)
returnfinishPoll(first); // 小于0则直接弹出执行first=null; 
if (leader!=null)
available.await();
else {
ThreadthisThread=Thread.currentThread();
leader=thisThread;
try {
available.awaitNanos(delay);  // 否则等待delay时间                    } finally {
if (leader==thisThread)
leader=null;
                    }
                }
            }
        }
    } finally {
if (leader==null&&queue[0] !=null)
available.signal();
lock.unlock();
    }
}

到这里大家发现没有,任务定时执行的思路和Timer其实是一样的,通过判断队首的定时任务的执行时间是否达到,达到则弹出执行,否则等待阻塞。

2.2.3 优缺点

优点:支持多定时任务并发执行;支持延迟执行;当前任务异常不会终止队列中后续任务执行。

缺点:不支持定时表达式;不支持分布式。

3 第三方定时任务调度框架

定时任务处理方式的核心思路其实都差不多,而第2章节介绍的是Java对其最原始最底层的处理方式,因此通过剖析源码的方式较为详细的介绍。但是实际企业生产使用过程中,他们并没有那么方便,比如不支持定时表达式,不支持界面化操作,不支持自动告警,不支持定时时间实时修改等,从而产生了大量的企业级定时任务框架,比如经典的Quartz,开源的xxl-job,阿里巴巴的schduleX等,这些框架通常功能丰富,但是往往又很重,对于轻量级应用而言引用并不划算,感兴趣的同学可以查阅更多资料详细了解。下面将介绍一款Spring自实现的定时任务调度方案Spring Scheduler。

4 Spring定时任务调度器

4.1 简介

为了简化使用者对动态任务的调度,Spring自实现了一个轻量级任务调度管理器进行动态任务管理与调度,使用者只需要在配置类上加入@EnableSchedule注解即可开启对计划任务的支持,然后在要执行计划任务的方法上加上@Schedule即可。Spring通过接口TaskScheduler和TaskExecutor这两个接口的方式为异步定时任务提供了一种抽象,前者拥有任务调度能力,后者拥有任务执行能力。

4.2 关键源码解析

4.2.1 @EnableSchedule注解到底做了什么

查看这个注解的元注解可以看到@Configuration和@Import(SchedulingConfiguration.class),@Import是用来导入配置类的,查看SchedulingConfiguration.class发现它向Spring容器声明了一个Bean:ScheduledAnnotationBeanPostProceessor,看一下这个类的解释:

* <p>This post-processor is automatically registered by Spring's

* {@code <task:annotation-driven>} XML element, and also by the

* {@link EnableScheduling @EnableScheduling} annotation.

这个类在初始化时主要会做两件事情:

(1)初始化TaskScheduler

TaskScheduler是实际任务的调度器。ScheduledAnnotationBeanPostProceessor这个类实现了ApplicationContextAware接口,重写了onApplicationEvent方法,这个方法会调用finishRegistration()方法,finishRegistration()方法在最后通过一个register调用了afterPropertiesSet()方法,这里先不讨论register是做什么的,afterPropertiesSet()方法第一步就是判断调度器是否为空,显然如果我们只单纯的加了一个@EnableSchedule注解,调度器为空,那么这里就会创建调度器,看这里创建的思路:

if (this.taskScheduler==null) {
this.localExecutor=Executors.newSingleThreadScheduledExecutor();
this.taskScheduler=newConcurrentTaskScheduler(this.localExecutor);
}

注意了,这里首先创建的是一个单线程的任务执行器,然后把这个执行器传递给新建的调度器,这个时候该任务调度器拥有了单线程任务执行能力,这也是为什么如果你只是单纯的引入此注解,多任务在执行的时候会发生阻塞。

(2)寻找所有加了@Scheduled和@Schedules注解的方法

postProcessAfterInitialization()方法会在所有bean初始化完后了找到所有加了@Scheduled和@Schedules注解的方法,并解析定时表达式进行任务初始化准备工作。

4.2.2 如何支持任务的并发执行性

从上面的分析中可以看出,@EnableSchedule注解默认对任务的执行采用的是单线程的方式,即所有任务都必须等待当前任务执行完成后才可以继续调度执行,在多任务系统中默认实现方式显然不能满足要求。如果需要支持多任务的并发执行,Spring也为我们提供了实现方式,实现SchedulingConfigurer接口,为调度器TaskScheduler创建自定义的任务执行器,创建方式如下:

@Configuration@EnableSchedulingpublicclassTaskScheduleConfigimplementsSchedulingConfigurer {
privateThreadPoolTaskSchedulertaskScheduler;
@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrartaskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
    }
@Bean(destroyMethod="shutdown")
publicThreadPoolTaskSchedulertaskScheduler(){
//创建一个线程池调度器taskScheduler=newThreadPoolTaskScheduler();
//设置线程池容量taskScheduler.setPoolSize(2);
//线程名前缀taskScheduler.setThreadNamePrefix("task-");
//等待时常taskScheduler.setAwaitTerminationSeconds(60);
//当调度器shutdown被调用时等待当前被调度的任务完成taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
//设置当任务被取消的同时从当前调度器移除的策略taskScheduler.setRemoveOnCancelPolicy(true);
//设置任务注册器的调度器returntaskScheduler;
    }
}

4.3,Spring如何实现任务异步化

Spring的定时任务执行都是同步的,有时候我们会碰到单任务执行时间很长的问题,需要将任务的执行异步化,可以将@Async注解和@Scheduled注解联合起来使用。但是这里会有一个问题,当任务被异步化的时候他会直接告诉任务调度器当前任务已经执行完了,因此下一次任务会根据定时时间准点执行,有可能实际上上一次任务并没有执行完。在某些场景下会导致数据异常,比如:

@Async@Scheduled(fixedRate=10*1000L)
publicvoidtaskMonitor() {
List<Task>taskList=taskBO.findUnfinishedTaskList();
for (Tasktask : taskList) {
executeInLock("monitor_"+task.getId(), () -> {
List<Subtask>subtaskList=taskBO.findUnfinishedSubtaskListByTaskId(task.getId());
if (subtaskList.isEmpty()) {
taskBO.completeTask(task);
            } else {
//未完成的subtask,true表示任务执行失败,记录错误log、发通知if (checkUnfinishSubtaskList(subtaskList)) {
logger.error("task execute fail:"+JSON.toJSONString(task));
taskBO.failTask(task, subtaskList);
sendDingtalk("任务执行超时,请关注:"+JSON.toJSONString(task));
                }
            }
returntrue;
        }, 8L, false);
    }
}

如果taskMonitor第一次没有执行完,针对某个taskA,正在执行completeTask方法,completeTask里面有insert DB和更新taskA状态的操作,但是数据还没有插入到DB,然后锁超时释放,这个时候taskMonitor又开始调度执行了,而taskA又被查询出来,再次执行completeTask操作,这样就会有重复数据插入到DB,因此尽量避免两个注解同时使用。如果确实需要将定时任务异步化,可以把锁的超时释放时间设置长一些,保证任务执行完在释放。

相关文章
|
20天前
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
12天前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
82 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
5天前
|
消息中间件 Java 应用服务中间件
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
12天前
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
161 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
13天前
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
50 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
1月前
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
78 22
解析静态代理IP改善游戏体验的原理
|
1月前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
101 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
4天前
|
存储 缓存 人工智能
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
本文深入解析了Java中`synchronized`关键字的底层原理,从代码块与方法修饰的区别到锁升级机制,内容详尽。通过`monitorenter`和`monitorexit`指令,阐述了`synchronized`实现原子性、有序性和可见性的原理。同时,详细分析了锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,结合对象头`MarkWord`的变化,揭示JVM优化锁性能的策略。此外,还探讨了Monitor的内部结构及线程竞争锁的过程,并介绍了锁消除与锁粗化等优化手段。最后,结合实际案例,帮助读者全面理解`synchronized`在并发编程中的作用与细节。
25 8
|
13天前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
36 5
|
19天前
|
监控 前端开发 Java
构建高效Java后端与前端交互的定时任务调度系统
通过以上步骤,我们构建了一个高效的Java后端与前端交互的定时任务调度系统。该系统使用Spring Boot作为后端框架,Quartz作为任务调度器,并通过前端界面实现用户交互。此系统可以应用于各种需要定时任务调度的业务场景,如数据同步、报告生成和系统监控等。
43 9

推荐镜像

更多