【JAVA定时器】四种常见定时器的原理和简单实现

2023-10-27

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

定时器顾名思义,即定时触发某个事件,分离开来,即包含三个因素:定时触发某个事件,本文也将以此为基础介绍五种常见的定时器


本文只做基于SpringBoot的示例,其余版本的请自行查阅资料,大同小异


1.介绍

1.1.目的

定时器的目的即为了在某个时间点,程序自身主动触发某个事件,而不需要外力去开启或者启动,以节省人力并统一管理

1.2.示例场景

  • 管理系统,需要每日12点将前一天的数据进行备份,并生成历史数据统计
  • 宿管系统,每日10点将所有未归人员统计出来,主动交由管理人员
  • 硬件设备,需要每隔2分钟检查设备是否连接正常,设备异常需要更新状态到管理端,必要时通知有关人员
  • 图书馆借书管理系统,每天12点需要检查即将超时和已超时归还的书籍,并通过短信或其他途径通知有关人员
  • 手机下载管理系统,开启下载后每隔0.5s刷新一次下载进度,在下载完成或者长时间卡顿时告知用户
  • 订单管理系统,用户下达订单后开需要在半小时内付款,成功付款则生成订单结果,超时未付款则自动取消订单

是不是觉得很常见?

1.3.常见实现方案

  • @Scheduled注解:基于注解
  • Timer().schedule创建任务:基于封装类Timer
  • 线程:使用线程直接执行任务即可,可以与thread、线程池、ScheduleTask等配合使用
  • quartz配置定时器:基于springquartz框架

本文仅简述前3种,比较简单易懂,quartz会专门分离出来整理


2.@Scheduled注解

2.1.介绍:

使用注解标记需要定时执行的方法,并设置执行时间,便可使其在指定的时间执行指定方法

2.2.步骤:

  1. 使用注解@Scheduled标记目标方法,参数为执行时间
  2. 使用注解@EnableScheduling标记目标方法所在的类,或者直接标记项目启动类

2.3.注解:

  • 注解@Scheduled为方法注解,用于标记某个方法在何时定时执行
  • 需要配合另一个注解@EnableScheduling进行使用,该注解用于标记某个类,开启定时任务,通常标记在定时器所在的类,或者直接设置在项目启动类上

2.4.@Scheduled参数:

  • @Scheduled(fixedDelay = 5000):方法执行完成后等待5秒再次执行

  • @Scheduled(fixedRate = 5000):方法每隔5秒执行一次

  • @Scheduled(initialDelay=1000, fixedRate=5000):延迟1秒后执行第一次,之后每隔5秒执行一次

  • fixedDelayStringfixedRateStringinitialDelayString:与上诉三种作用一直,但参数为字符串类型,因而可以使用占位符,形如

    @Scheduled(fixedDelayString = "${time.fixedDelay}")
    
  • @Scheduled(cron = "0 0,30 0,8 ? * ? "):方法在每天的8点30分0秒执行,参数为字符串类型,那么同理也可使用占位符,cron表达式请另行查阅资料,推荐看这篇文章:https://www.jianshu.com/p/1defb0f22ed1

2.5.示例

示例1:每隔3秒执行一次

@Component
@EnableScheduling
public class ScheduleTest {

    private int count = 0;

    /**
     * 每3秒钟执行一次
     */
    @Scheduled(cron = "*/3 * * * * ?")
    public void test1() {
        System.out.println(count + ":" + (new Date()).toString());
        count++;
    }
}

image-20200907123608014

示例2:第一次等待10秒,之后每3秒一次

@Component
@EnableScheduling
public class ScheduleTest {

    private int count = 0;

    /**
     * 第一次等待10秒,之后每3秒钟执行一次
     */
    @Scheduled(initialDelay = 10000, fixedRate = 3000)
    public void test1() {
        System.out.println(count + ":" + (new Date()).toString());
        count++;
    }

}

image-20200907124022947

2.6.小结

  • 优势:简单便捷,仅两行注解便完成了定时效果
  • 劣势:所有参数和执行的方法必须提前写入代码里,可扩展性极低

3.Timer().schedule创建任务

3.1.样例

使用非常简单,这里先给出样例,在对照进行介绍

代码如下

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

@Component
public class TimerTest {
    private Integer count = 0;

    public TimerTest() {
        testTimer();
    }

    public void testTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, 0, 1000);
    }
}

执行结果

image-20210225092748479

可以看到每隔1s打印一次count并自增1


3.2.介绍

核心包括Timer和TimerTask,均为jkd自带的工具类,代码量分别为721行和162行(包括注释),都不多,有兴趣的可以直接看看源码

3.2.1.TimerTask

TimerTask实际上就是一个Runnable而已,继承Runnable并添加了几个自定义的参数和方法,没啥好介绍的,有兴趣可以看源码

3.2.2.Timer

Timer字面意思即定时器,为jkd自带的工具类,提供定时执行任务的相关功能


实际上包括三个类:

  • Timer:即定时器主类,负责管理所有的定时任务,每个Timer拥有一个私有的TaskQueueTimerThread

  • TaskQueue:即任务队列,Timer生产任务,然后推到TaskQueue里存放,等待处理,被处理掉的任务即被移除掉

    TaskQueue实质上只有一个长度为128的数组用于存储TimerTask、一个int型变量size表示队列长度、以及对这两个数据的增删改查

  • TimerThread:即定时器线程,线程会共享TaskQueue里面的数据,TimerThread会对TaskQueue里的任务进行消耗

    TimerThread实际上就是一个Thread线程,会不停的监听TaskQueue,如果队列里面有任务,那么就执行第一个,并将其删除(先删除再执行)


流程分析

  • Timer生产任务(实际上是从外部接收到任务),并将任务推到TaskQueue里面存放,并唤醒TaskQueue线程(queue.notify()
  • TimerThread监听TaskQueue,若里面有任务则将其执行并移除队里,若没有任务则让队列等待(queue.wait()

这么一看,这不就是典型的==生产者/消费者模式==,timer负责生产(实际上是接受),而TimerThread负责消费,TaskQueue作为中转仓库


构造方法

构造的时候会设置定时器线程的名字并将其启动

完整格式如下,其中两个参数均可缺省

public Timer(String name, boolean isDaemon)
  • name:即线程名,用于区分不同的线程,缺省的时候默认使用"Timer-" + serialNumber()生成唯一线程名
  • isDaemon:是否是守护线程,缺省的时候默认为否,有啥区别请自行了解,有机会的话我也会整理笔记

核心方法

核心方法有添加任务、取消任务和净化三种

  • 添加任务有6中公用方法(实际最后使用同一种私有方法)

    • schedule(TimerTask task, long delay):指定任务task,在delay毫秒延迟后执行
    • schedule(TimerTask task, Date time):指定任务task,在time时间点执行一次
    • schedule(TimerTask task, long delay, long period):指定任务task,延迟delay毫秒后执行第一次,并在之后每隔period毫秒执行一次
    • schedule(TimerTask task, Date firstTime, long period):指定任务task,在firstTime的时候执行第一次,之后每隔period毫秒执行一次
    • scheduleAtFixedRate(TimerTask task, long delay, long period):作用与schedule一致
    • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):作用与schedule一致

    实际上最后都会使用sched(TimerTask task, long time, long period),即指定任务task,在time执行第一次,之后每隔period毫秒执行一次

    schedule使用系统时间计算下一次,即System.currentTimeMillis()+period

    scheduleAtFixedRate使用本次预计时间计算下一次,即time + period

    对于耗时任务,两者区别较大,请按需求选择,瞬时任务无区别

  • 取消任务方法:cancel(),会将任务队列清空,并堵塞线程,且不再能够接受任务(接受时报错),并不会销毁本身的实例和其内部的线程

  • 净化方法:purge(),净化会将队列里所有被取消的任务移除,对剩余任务进行堆排序,并返回移除任务的数量

补充

  • 如何保证第一个任务是执行时间最早的

    任务队列会在每一次添加任务和删除任务时,进行堆排序矫正,净化也会对剩余任务重新堆排序

  • cancel的时候线程如何处理

    定时器线程进行堵塞处理,并没有销毁,在执行当前任务后就不会执行下一次了,但是**线程并没有销毁**

    所以尽量不要创建太多timer对象,会增加服务器负担

3.3.使用步骤

  1. 初始化Timer

    Timer timer=new Timer();
    
  2. 初始化task

    private class MyTask extends TimerTask {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    MyTask myTask=new MyTask();
    
  3. 添加任务

    timer.schedule(myTask, 5000, 3000);
    

    完整代码:

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    @Component
    public class TimerTest {
        private Integer count = 0;
    
        public TimerTest() {
            testTimer2();
        }
    
        public void testTimer2() {
            Timer timer = new Timer();
            MyTask myTask = new MyTask();
            timer.schedule(myTask, 0, 1000);
        }
    
        private class MyTask extends TimerTask {
            @Override
            public void run() {
                try {
                    //do Something
                    System.out.println(new Date().toString() + ": " + count);
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    

    当然可以缩写为样例里面的写法,更加简洁,请按照自己需求修改

4.线程

线程应该是最常见的实现方案,创建一个线程执行任务即可,举例几个不同的写法,代码如下

4.1.使用thread + runnable

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class ThreadTest {

    private Integer count = 0;

    public ThreadTest() {
        test1();
    }

    public void test1() {
        new Thread(() -> {
            while (count < 10) {
                System.out.println(new Date().toString() + ": " + count);
                count++;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

4.2.使用线程池 + runnable

package com.yezi_tool.demo_basic.test;

import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
public class ThreadTest {

    private static final ExecutorService threadPool = Executors.newFixedThreadPool(5);// 线程池
    private Integer count = 0;

    public ThreadTest() {
        test2();
    }

    public void test2() {
        threadPool.execute(() -> {
            while (count < 10) {
                System.out.println(new Date().toString() + ": " + count);
                count++;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

4.3.使用ScheduledTask + runnable

ScheduledTask 有11种添加任务的方法,详情直接查看文件TaskScheduler.java,这里给出常用的几个示例

  • 设置触发频率为3000毫秒

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.scheduling.TaskScheduler;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class ThreadTest {
    
        private Integer count = 0;
        private final TaskScheduler taskScheduler;
    
        public ThreadTest(TaskScheduler taskScheduler) {
            this.taskScheduler = taskScheduler;
            test3();
        }
    
        public void test3() {
            taskScheduler.scheduleAtFixedRate(() -> {
                System.out.println(new Date().toString() + ": " + count);
                count++;
            }, 3000);
        }
    }
    
  • 设置触发时间为每天凌晨1点

    package com.yezi_tool.demo_basic.test;
    
    import org.springframework.scheduling.TaskScheduler;
    import org.springframework.scheduling.support.CronTrigger;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class ThreadTest {
    
        private Integer count = 0;
        private final TaskScheduler taskScheduler;
    
        public ThreadTest(TaskScheduler taskScheduler) {
            this.taskScheduler = taskScheduler;
            test4();
        }
    
        public void test4() {
            taskScheduler.schedule(() -> {
                System.out.println(new Date().toString() + ": " + count);
                count++;
            }, new CronTrigger("0 0 1 * * ?"));
        }
    }
    

5.quartz

专门整理了一篇quartz的笔记,有兴趣的可以看我上一篇博客

写的并不完善,后续应该会进行修正

6.总结

  • @schedule使用方便快捷,但功能有限,扩展性极低,适用于不需要统一管理的简单场景
  • Timer可以统一管理定时任务,但自身作为一个工具类,功能较少,但是也适用于很多场景了
  • 线程的使用同样比较方便,灵活度特别高,支持各种类型的触发时间,但毕竟没有专用的框架,功能并不算特别齐全,适用于对自由度要求较高的场景
  • quartz作为专门的定时器项目,功能齐全且强大,目前大部分项目仍只使用了其小部分功能,适用于要求较高的场景

7.demo地址

https://gitee.com/echo_ye/demo_basic/tree/scheduleDemo

不同定时器启用方法在README.MD中查看,一共6种方法,如有纰漏请联系我

仅实现了部分功能作为样例,请按照需求自己扩展哦,有疑问或者建议欢迎联系我~


BB两句

其实除了@schedule,其余的都可以自定义管理器,来统一管理,并动态修改,具体咋做此处先不做赘述

quartz已经整理除了静态定时器和动态定时器,有兴趣的可以瞅瞅



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【JAVA定时器】四种常见定时器的原理和简单实现 的相关文章

随机推荐

  • MVC设计模式--HeadFirst学习

    1 模型视图设计器模式 是可重用性很高的基于多个模式创建出来的复合模式 其中模型负责数据状态和逻辑 视图负责显示和交互 控制器负责传达视图要求模型需要执行的命令 而模型和视图之间是观察者模式关系 视图和控制器之间是策略模式关系 视图本身通过
  • linux串口操作

    man termios http www man7 org linux man pages man4 tty ioctl 4 html 串行口是计算机一种常用的接口 具有连接线少 通讯简单 得到广泛的使用 常用的串口是RS 232 C接口
  • WIN7 64位操作系统 编译64位OSG的方法

    1 在OSG官网上下载OSG源代码 本人下载的是OSG3 4 0 http www openscenegraph org index php download section stable releases 2 下载第三方包 http ww
  • ROS 与 ubuntu

    欢迎访问我的博客首页 ROS 与 ubuntu 1 安装 ROS Noetic 1 1 源和钥匙 1 2 安装 1 3 编译依赖 1 4 启用 ROS 2 ROS 教程 2 1 常用命令 2 2 功能 3 ubuntu 常用命令 4 参考
  • Linux删除用户

    libai为用户名 创建用户 useradd libai 设密码 passwd libai 删除用户 userdel r libai
  • 学了Python,可以做哪些月入过万的兼职?

    如果学了Python不做全职工作 那么可以靠哪些兼职赚到钱 今天我们就来看看一位有着4年开发经验的老鸟的分析回答 希望对你有所帮助 emmm 以我差不多四年的 Python 使用经验来看 大概可以按以下这些路子来赚到钱 但编程技能其实只是当
  • Vue2.x入门篇

    文章目录 一 Vue js简介 1 1 MVC和MVVM设计模式 1 MVC设计模式 2 MVVM设计模式 1 2 Vue js的下载 二 Vue实例 2 1 Vue实例 2 2 生命周期 2 3 模板数据 2 4 过滤器 三 计算属性 方
  • 大话数据结构 1 绪论

    数据 是描述客观事物的符号 是计算机中可以操作的对象 是能被计算机识别 并输入给计算机处理的符号集合 数据元素 是组成数据的 有一定意义的基本单位 在计算机中通常作为整体处理 也被称为记录 数据项 一个数据元素可以由若干个数据项组成 数据项
  • 25B无人直升机调试(Tuning)

    要知道的直升机原理 首先要掌握的5个飞行模式 本文首先调试自稳模式 Stabilize 自稳模式 Alt Hold 定高模式 Loiter OF loiter 悬停模式 RTL Return to Launch 返航模式 Auto 自动模式
  • jdbctypetimestamp,getTimestamp()在MySQL JDBC连接器中进行两次时区转换?

    I have a column of type DATETIME wiht a value of 2012 05 07 19 59 12 in MySQL database I m trying to retrieve this value
  • OpenCV-Python击中击不中变换案例:真图作核的绳网结匹配

    前往老猿Python博客 https blog csdn net LaoYuanPython 一 引言 在前面的博文介绍了HMT变换的基础知识及理解HMT变换的关键知识点以及长方形和绿叶边界的提取 本文将简单介绍另一个案例 绳网结匹配 老猿
  • 2023年Java毕业设计题目推荐,怎样选题?500道毕业设计题目推荐

    大家好 我是程序员徐师兄 最近有很多同学咨询 说毕业设计了 不知道选怎么题目好 有哪些是想需要注意的 今天 我整理了一些Java毕业设计的题目 可以参考一下 希望对大家有所帮助 文章目录 一 大体实现思路 二 如何避坑 三 可借鉴题目参考
  • 计算机中¥符号按哪个键,人民币键盘符号怎么打 电脑怎么打人民币符号

    原标题 小编教你电脑怎么打人民币符号 关于电脑问题教程分享 来源 191路由网 编辑 小元 对于财务人员来说 货币的符号熟悉程度已经是滚瓜烂熟了 特别对于人民币符号不仅要学会手写 在办公软件上还要知道怎么拼写出来 但是对于新手而已就需要学习
  • 关于在VMware上安装Android x86及FTP详细使用

    关于在VMware上安装Android x86产生的一系列问题及解决办法 下载地址 安装配置过程 配置磁盘 配置启动文件 关于分辨率问题 兼容ARM 输入法问题 FTP服务 打开FTP服务 搭建服务器 客户端连接 部分问题 结尾 下载地址
  • go 进阶 http标准库相关: 三. HttpServer 服务启动到Accept等待接收连接

    目录 一 http ListenAndServe 服务启动基础概述 二 Server 结构体详解 三 查看Server Serve ln 源码 连接的state状态 四 Listener Accept 等待连接 问题 六 总结 引出一个小问
  • TARS快速入门(NodeJs)

    1 前言 一个NodeJs应用程序框架需要关注很多问题 如自动发布 多机部署 负载均衡 监控报警 日志的输出与管理 服务异常重启等等 而Tars应用程序框架就提供了解决这些问题的一整套方案 Tars是腾讯从2008年到今天一直在使用的后台逻
  • mysql column_MySQL,`column` like '一些文字' 是否就等于 `column` = '一些文字',有什么区别是呢?...

    建议你用 EXPLAIN 语句来检查一下这两条语句的的执行结果 下面是我在自己机器上用 phpMyAdmin 的性能分析工具做的测试 第一条 SELECT FROM Affiliate Impression Log WHERE Produc
  • 【C++】CGAL学习笔记

    一 HELLO WORLD 1 官方文档 CGAL TUTORIALS 2 所有 CGAL 头文件都在子目录中 所有 CGAL 类和函数都在命名空间中 类以大写字母开头 全局函数以小写字母开头 常量全部大写 3 几何图元 如点 线等都定义在
  • Oracle transport tablespace

    本来没想过发布这个文章 只是周边有一朋友工作中遇到合并数据库的情况 他是通过expdp提取出五个库对象 然后impdp到新库里面 我觉得这种方法特别耗时 尤其在数据量比较大的时候 这种时候我觉得采用表空间传输方法效率更高点 当然我们还可以使
  • 【JAVA定时器】四种常见定时器的原理和简单实现

    个人学习笔记分享 当前能力有限 请勿贬低 菜鸟互学 大佬绕道 如有勘误 欢迎指出和讨论 本文后期也会进行修正和补充 前言 定时器顾名思义 即定时触发某个事件 分离开来 即包含三个因素 定时 触发 某个事件 本文也将以此为基础介绍五种常见的定