热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

javatimer序列化_Java主要三种方式来实现定时任务

现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了。很多业务需求的实现都

现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了。

很多业务需求的实现都离不开定时任务,例如,每月一号,移动将清空你上月未用完流量,重置套餐流量,以及备忘录提醒、闹钟等功能。

Java 系统中主要有三种方式来实现定时任务:

Timer和TimerTask

ScheduledExecutorService

三方框架 Quartz

下面我们一个个来看。

(图1)

27ad69827cf87ffecb2ba8c7b3cca89f.png

Timer和TimerTask

先看一个小 demo,接着我们再来分析其中原理:

这种方式的定时任务主要用到两个类,Timer 和 TimerTask。其中,TimerTask 继承接口 Runnable,抽象的描述一种任务类型,我们只要重写实现它的 run 方法就可以实现自定义任务。

而 Timer 就是用于定时任务调度的核心类,demo 中我们调用其 schedule 并指定延时 1000 毫秒,所以上述代码会在一秒钟后完成打印操作,接着程序结束。

那么,使用上很简单,两个步骤即可,但是其中的实现逻辑是怎样的呢?

Timer 接口

首先,Timer 接口中,这两个字段是非常核心重要的:

(图2)

abdb779c836a363471204d6874882763.png

TaskQueue 是一个队列,内部由动态数组实现的最小堆结构,换句话说,它是一个优先级队列。而优先级参考下一次执行时间,越快执行的越排在前面,这一点我们回头再研究。

接着,这个 TimerThread 类其实是 Timer 的一个内部类,它继承了 Thread 并重写了其 run 方法,该线程实例将在构建 Timer 实例的时候被启动。

run 方法内部会循环的从队列中取任务,如果没有就阻塞自己,而当我们成功的向队列中添加了定时任务,也会尝试唤醒该线程。

我们也来看一下 Timer 的构造方法:public Timer(String name) {

thread.setName(name);

thread.start();

}

再简单不过的构造函数了,为内部线程设置线程名,并启动该线程。

最后,我们着重看一下 Timer 中用于配置一个定时任务进任务队列的方法。//在时刻 time 处执行任务

schedule(TimerTask task, Date time)

//延时 delay 毫秒后执行任务

schedule(TimerTask task, long delay)

//固定延时重复执行,firstTime为首次执行时间,

//往后没间隔 period 毫秒执行一次

schedule(TimerTask task, Date firstTime, long period)

//固定延时重复执行

//首次执行时间为当前时间延时 delay 毫秒

schedule(TimerTask task, long delay, long period)

//固定频率重复执行,每过 period 毫秒执行一次

scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

//固定频率重复执行

scheduleAtFixedRate(TimerTask task, long delay, long period)

相信有了注释,这几个方法的区别与作用应该不难理解,但是其中有两个概念需要作一点区分。

==固定延时== VS ==固定频率==

固定延时:以任务的上一次 实际 执行时间做参考,往后延时 period 毫秒。

固定频率:任务的往后每一次执行时间都在任务提交的那一刻得到了确定,不论你上次任务是否意外延时了,定时定点执行下一次任务。

这两者的区别还是很大的,希望你能够理解清楚,接着我们以其中一个方法为例,看看底层实现。

以这个方法为例,其他重载方法的底层调用都是同样的,我们不去赘述。

(图3)

a19527a26d9bae1bb70e037c168ec91b.png

这个方法的作用,我们再说一遍。

以当前时间为准,延时 delay 毫秒后第一次执行该任务,并且采取固定延时的方式,每隔 period 毫秒再次执行该任务。

开头的两个异常判断我们不再赘述,看看 sched 方法:

(图4)

d916e2c972d130870b4f4e61546b549b.png

方法需要传入三个参数,参数 task 代表的需要执行的任务体,TimerTask 我们回头会详细介绍,这里你知道它代表了一个任务体即可。

参数 time 描述了该任务下一次执行的时刻,计算机底层是以毫秒描述时刻的,所以这里转换为 long 类型来描述时刻。

参数 period 是固定延时的毫秒数。

整个方法的逻辑我们可以总结概括一下,具体的代码就不一行行分析了,因为也不难。

首先使用任务队列的内置对象锁,锁住个队列。

接着再去锁住我们的 task,并修改其内部的一些属性字段值,nextExecutionTime 指明下一次任务执行时间,period 设置固定延时的毫秒数,修改 state 状态为计划中。

然后将 task 添加到任务队列,其中 add 方法内部会进行最小堆重构,参考的就是 nextExecutionTime 字段的值,越小优先级越高。

判断如果自己就是队列第一个任务,那么将唤醒 Timer 中阻塞了的任务线程。

可能会有人疑问,Timer 如何判断一个任务是否是重复执行的,还是单次执行就结束的?

答案在 TimerThread 的 run 方法里,有兴趣你可以去研究下,方法体比较多比较长,这里不做分析。

当我们构造 Timer 实例的时候,就会启动该线程,该线程会在一个死循环中尝试从任务队列上获取任务,如果成功获取就执行该任务并在执行结束之后做一个判断。

如果 period 值为零,则说明这是一次普通任务,执行结束后将从队列首部移除该任务。

如果 period 为负值,则说明这是一次固定延时的任务,修改它下次执行时间 nextExecutionTime 为当前时间减去 period,重构任务队列。

如果 period 为正数,则说明这是一次固定频率的任务,修改它下次执行时间为 上次执行时间加上 period,并重构任务队列。

其实,我也已经把 TimerThread 的 run 方法里最核心的逻辑也已经介绍了,建议大家亲自去研究研究具体代码的实现,你会对这一块的逻辑更清晰。

最后,我们看一看这个 Timer 它有哪些劣势的地方:

Timer 的背后只有一个线程,不管你有多少个任务,都只有一个工作线程,效率上必然是要打折扣的。

限于单线程,如果第一个任务逻辑上死循环了,后续的任务一个都得不到执行。

依然是由于单线程,任一任务抛出异常后,整个 Timer 就会结束,后续任务全部都无法执行。

所以你看,单线程的 Timer 带来了太多局限性,于是我们看它的替代者。

PS:本来计划再介绍下 TimerTask 这个抽象任务类的,但是发现实在没啥好介绍的,就是增加了两个字段,一个用于记录下一次该任务的执行时间,一个用于延时毫秒数。你也只需要重写其 run 方法即可。

ScheduledExecutorService

这个接口相信你一定眼熟,我告诉你在哪见过。

(图5)

88f188512ee6914c0585bbe9d8124c92.png

你看,它是我们异步框架中的接口,正好我们今天来介绍他,这样整个异步框架中所有的接口我们都分析过了。

(图6)

6c4b1bc18a81939c0d39ddd103bf17ae.png

(图7)

346c9d658a910871f94a17b1537ca1b4.png

ScheduledExecutorService中定义的这四个接口方法和 Timer 中对应的方法几乎一样,只不过 Timer 的 scheduled 方法需要在外部传入一个 TimerTask 的抽象任务。

而我们的 ScheduledExecutorService 封装的更加细致了,随便你传 Runnable 或是 Callable,我会在内部给你做一层封装,封装一个类似 TimerTask 的抽象任务类(ScheduledFutureTask)。

然后传入线程池,启动线程去执行该任务,而我们的 ScheduledFutureTask 重写的 run 方法是这样的:

(图8)

8f0b04cde0544d7dc01afb673550cc37.png

如果 periodic 为 true 则说明这是一个需要重复执行的任务,否则说明是一个一次性任务。

所以实际执行该任务的时候,需要分类,如果是普通的任务就直接调用 run 方法执行即可,否则在执行结束之后还需要重置下下一次执行时间。

整体来说,ScheduledExecutorService 区别于 Timer 的地方就在于前者依赖了线程池来执行任务,而任务本身会判断是什么类型的任务,需要重复执行的在任务执行结束后会被重新添加到任务队列。

而对于后者来说,它只依赖一个线程不停的去获取队列首部的任务并尝试执行它,无论是效率上、还是安全性上都比不上前者。

所以,建议使用 ScheduledExecutorService 取代 Timer,当然,通过学习 Timer 会更有助于对 ScheduledExecutorService 的研究。

三方框架 Quartz

除了上述两种定时任务框架外,Java 生态圈还存在一种开源的三方框架,他就是 Quartz。

Quartz 是一个功能完善的任务调度框架,支持集群环境下的任务调度,需要将任务调度状态序列化到数据库。

Quartz 已经是随着分布式概念的流行,成为企业级定时任务调度框架中的不二选择。

Quartz 这个框架的使用及与原理在本篇就不做介绍了,我们会在后续介绍分布式概念的时候再来介绍它与 SpringCloud 平台下的整合使用情况。



推荐阅读
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 解决java.lang.IllegalStateException: ApplicationEventMulticaster not initialized错误的方法和原因
    本文介绍了解决java.lang.IllegalStateException: ApplicationEventMulticaster not initialized错误的方法和原因。其中包括修改包名、解决service name重复、处理jar包冲突和添加maven依赖等解决方案。同时推荐了一个人工智能学习网站,该网站内容通俗易懂,风趣幽默,值得一看。 ... [详细]
  • 判断数组是否全为0_连续子数组的最大和的解题思路及代码方法一_动态规划
    本文介绍了判断数组是否全为0以及求解连续子数组的最大和的解题思路及代码方法一,即动态规划。通过动态规划的方法,可以找出连续子数组的最大和,具体思路是尽量选择正数的部分,遇到负数则不选择进去,遇到正数则保留并继续考察。本文给出了状态定义和状态转移方程,并提供了具体的代码实现。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
author-avatar
侯faulds_534
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有