JAVAEE—实现多线程版本的定时器
文章目录
- 什么是定时器
- 定时器的概念
- 定时器的简单应用和介绍
- 代码示例
- 定时器的代码解析
- 定时器在执行任务的时候是创建了一个线程去执行吗?
- 为什么叫做扫描线程呢?
- 执行完任务之后代码就暂停了不自动结束吗?
- 手撕定时器demo
- 相对时间与绝对时间
- Mytime类
- 为什么定时器必须我们手动结束代码的运行
- 改进版本
- 为什么不用阻塞队列来实现呢?
什么是定时器
定时器的概念
首先定时器是什么呢?
定时器是我们在开发中比较常用的一个组件,类似于一个闹钟,当某个你设置的时间到了之后就开始去执行特定的任务。
定时器的简单应用和介绍
代码示例
import java.util.Timer;
import java.util.TimerTask;public class Main {public static void main(String[] args) {Timer t1=new Timer();t1.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2秒以后打印");}},2000);System.out.println("我爱老婆");}
}
首先我们来看一下这个代码,Timer是java自身所拥有的的一个定时器类,而schedule是这里面的一个方法,方法的形参如图
根据图中我们可以看到这里面有一个TimerTask类还有一个long类型的整数,那么第一个是什么类呢?我们再往深一些去介绍
我们可以看到这个类是继承了Runnable接口的那么按照我们之前的学习这里面肯定有一个run方法,因此我们可以得出这里面的第一个参数我们传递的其实就是要进行的方法是什么,第二个参数就是我们设置的时间是在多少时间以后,那么上面写的实列代码表示的意思就是,两秒之后执行任务我们看一下代码运行结果。
这是运行截图我们可以看到两个现象,
首先,在执行任务期间,并不是主线程的代码需要等待其执行完毕后再继续执行整个过程感觉更像是多线程的模式执行的
其次:在执行任务之后当一切都打印完成后我们的程序并没有按照我们想象的那样退出而是在那里暂停了。
这是为什么呢?那么我们就来解析一下定时器的内部代码
定时器的代码解析
首先上面的两个问题我们逐个回答首先第一个
定时器在执行任务的时候是创建了一个线程去执行吗?
是的没错,定时器在执行任务的时候其实是创建了一个线程执行的。这个线程在我们创建对象的时候就已经创建好了,这个线程的名字我们称之为扫描线程。
为什么叫做扫描线程呢?
这里我先请大家想一个问题就是你为了能早其会不会订多个闹钟把自己叫醒,我相信即使你自己可能不会但是你身边一定有这种人对不对。那么这时候我们就要明白一个问题了定时器这个东西并不是只能定一个时间,而是可以定多个时间多个任务,我们只需要在到达某个时间之后执行这个需要执行的任务就可以了。那么既然我们可以定多个任务多个闹钟的话我们该用什么容器去存储呢?
有的同学说是顺序表?此言差亦假如说我们用了顺序表那么每次我想知道那个任务到达该执行的时间了我岂不是还需要一个任务一个任务的去遍历吗?那假如说我们有很多个定时任务这可该怎么办呢?因此顺序表这个办法行得通但是效率太低了。
那么我们到底该用什么办法呢?难道就要你失败了吗?就要止步不前了吗?不肯定不是那么容易被打败的。
这时候我们想起来既然我们加入的是时间那么,时间在计算机眼中就是一个整数,既然如此那他肯定是可以比较大小的既然可以比较大小那就好办了我只需要用一个堆来进行存储就可以了我简直太聪明了,而事实也正是如此。我们存储任务用的数据结构就是堆。
而扫描线程就是不断的获取堆顶元素,然后查看堆顶的元素是否达到了执行的条件。从而开始执行任务。
正因为他不断的扫描因此我们叫做扫描线程。
执行完任务之后代码就暂停了不自动结束吗?
是的当我们的任务执行完了之后代码就暂停了不会自动结束需要我们手动结束这时候就有同学蒙蔽了,啊?这是为啥?那么我们接下来来动手实现以下这个定时器就知道原因了。
手撕定时器demo
首先我们先来看看库里面怎么实现的。
首先库里面的定时器的名字叫做Timer,然后schedule方法中的对象名字叫做Timetask因此我们也实现这两个类。那么首先我们先实现Timetask类,那么我们先来看看库里面这个类的构造方法。
在这里面我们设置了两个变量一个是记录时间的time还有一个就是我们的runnable。然后构造方法就是将传入的runnable赋值给类中的runnable然后主要是这个时间是怎么回事为什么要这样去写的呢?这就涉及到两个概念了。那就是相对时间与绝对时间的概念
相对时间与绝对时间
什么是相对时间呢?相对时间就是我们说的几秒几秒之后,这些几秒之后都是相对于我们现在的时间的说法因此叫做相对时间那么什么是绝对时间呢?绝对时间的意思就是我们说的,几点几分几秒这种叫做绝对时间,那么我们在使用的时候是用相对时间好呢?还是绝对时间好呢?我们可以带入我们生活中的列子,我们在生活中肯定是绝对时间要相对于相对时间是更加准确的,其次我们可以想一下自己定闹钟的时候是定一个几小时之后比较方便还是顶一个确定的几点几分比较方便呢?很显然是后者比较方便,那么如此我们就知道了这两个概念,那么我们就可以解释以下这个代码了。
System.currentTimeMillis()这个方法是用来获取我们当前的时间的我们来看一下这个方法的返回值如下图
我们看到这个方法的返回值是一个long类型的整数我们来打印以下。
import java.util.Timer;
import java.util.TimerTask;public class Main {public static void main(String[] args) {System.out.println(System.currentTimeMillis());try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(System.currentTimeMillis());}
}
运行结果
我们可以看出来这个方法可以获取当前的时间并将其转换为秒数。因此我们的初始化方法可以先获取当前的时间再加上我们想要定时的秒数然后获取到绝对时间从而进行初始化。因此这里的初始化是这样的格式。
Mytime类
接下来我们来实现Mytimer。那么在实现之前我们依旧先来分析一下怎么实现构造函数. 那么我们首先思考一下上面说的那句话那就是定时器内部是存在一个扫描线程的这个扫描线程从我们创建这个类的实列化对象的时候这个线程就已经创建好了说明这个线程肯定是在构造方法中被创建的, 既然如此, 我们就可以得出在构造方法中创建线程并且让这个线程不断的运行着. 由此我们的代码初步就可以写成下面的样子
import java.util.PriorityQueue;public class Mytimer {private PriorityQueue<MyTimerTask>queue=new PriorityQueue<>();public Mytimer(){Thread t1=new Thread(()->{while(true){}});}public void schedule(Runnable runnable,long time){queue.offer(new MyTimerTask(runnable,time));}
}
首先构造函数创造线程, schedule方法将要执行的任务入队列, 那么接下来呢?很简单其实, 我们只需要让扫描线程不断的从这个堆里取出堆顶元素并判断是否可以执行,如果可以执行就执行如果不可以执行就不执行. 那么该怎么实现呢?首先就是我们要实现一个多线程版本的那么我们肯定要明白一个事情那就是当我们的扫描线程发现这个线程为空的时候要进行等待那么按照我们上一篇将的生产消费者模型我们进行等待可以使用while循环判断当其为空的时候就一直等待
那么既然需要等待那就肯定需要上锁那么代码就是
好的那么等待的话我们入队列这个操作也需要上锁因为我们实现的是多线程版本的,因此为了保证安全我们需要堆入队列这个操作也上锁.
然后当我们入队列之后发现此时队列肯定不是空了我们就可以唤醒线程让其获取堆头的任务看看是否需要完成.
当我们判断堆顶任务需要执行后我们就使其执行,执行之后将其从堆中去除即可.我们用代码来试一下现在写的程序
import java.util.Objects;
import java.util.PriorityQueue;public class Mytimer {private Object locker=new Object();private PriorityQueue<MyTimerTask>queue=new PriorityQueue<>();public Mytimer(){Thread t1=new Thread(()->{try {while(true){synchronized (locker){while(queue.isEmpty()){locker.wait();}//当可执行到这里的时候说明线程不为空我们需要进行一个操作就是取堆顶MyTimerTask myTimerTask=queue.peek();long nowTime=System.currentTimeMillis();if(nowTime>=myTimerTask.getTime()){Runnable runnable=myTimerTask.getRunnable();runnable.run();queue.poll();}else{}}}} catch (InterruptedException e) {e.printStackTrace();}});t1.start();}public void schedule(Runnable runnable,long time){synchronized (locker){queue.offer(new MyTimerTask(runnable,time));locker.notify();}}
}
Test
import java.util.Timer;
import java.util.TimerTask;public class Main {public static void main(String[] args) {Mytimer mytimer=new Mytimer();mytimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}},2000);mytimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}},1000);mytimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}},3000);}
}
运行截图
那么现在回到上面的那个问题
为什么定时器必须我们手动结束代码的运行
相信写道这里大家就明白为什么了因为我们线程里的任务写的是while(true)并且当堆为空的时候我们的线程是在不停的等待的所以肯定不能自己结束需要我们手动才可以结束的啊.
改进版本
那么写道这里我们发现我们的目的能完成了我们的任务也正常显示了可是这里面有一个不完美的地方如图
那就是这个代码,我们举个例子假如说我们接下来要执行的任务是14:30分执行的那么现在没到这个时间的时候我们这个while循环岂不是一种都在运行着并且一直都是else然后什么也不干,这对我门的cpu资源是一个很大的浪费因此我们可以改一下怎么改呢?那就是wait的另一种用法.wait(最大等待时间) 那么代码如下图
在这里我们执行wait就可以了,那么有同学就会疑惑为什么不用sleep呢?因为如果用sleep的话我们就必须等到sleep结束可实际上呢假如说此时我们的堆顶元素是14:30执行的此时是14:09分不执行然后让他一直沉睡到14:30那么在其sleep的过程中加入我插入一个14:10分执行的任务的话无法将其唤醒就会导致这个14:10分的任务无法执行这是不被允许的.
为什么不用阻塞队列来实现呢?
因为我们知道这里的等待有两处而我们也希望可以用一个notify去唤醒这两处等待,但是阻塞队列内部已经有wait和notify了如果用notify 的话就会导致我们实现这样的两处等待一个notify唤醒就比较麻烦.
相关文章:

JAVAEE—实现多线程版本的定时器
文章目录 什么是定时器定时器的概念定时器的简单应用和介绍代码示例 定时器的代码解析定时器在执行任务的时候是创建了一个线程去执行吗?为什么叫做扫描线程呢?执行完任务之后代码就暂停了不自动结束吗? 手撕定时器demo相对时间与绝对时间Myt…...
KY228 找位置(用Java实现)
描述 对给定的一个字符串,找出有重复的字符,并给出其位置,如:abcaaAB12ab12 输出:a,1;a,4;a,5;a,10,b,2&…...

物联网边缘网关有哪些优势?-天拓四方
随着物联网技术的快速发展,越来越多的设备接入网络,数据交互日益频繁,对数据处理和传输的要求也越来越高。在这样的背景下,物联网边缘网关应运而生,以其低延迟、减少带宽消耗、提高数据质量和安全性等优势,…...
【C++】6-2 交换函数2 分数 10
6-2 交换函数2 分数 10 全屏浏览 切换布局 作者 刘利 单位 惠州学院 根据题目需求,编写一个交换函数Swap。 裁判测试程序样例: #include <iostream> using namespace std; class pen{private:string brand;string color;double price;publi…...

kafka 01
01....

Linux离线安装Docker-Oracle_11g
拉取oracle11g镜像 docker pull registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g创建11g容器 docker run -d -p 1521:1521 --name oracle11g registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g查看容器是否创建成功 docker ps -a导出oracle容器,查看…...
web前端3.19
一、属性选择器与伪类选择器 属性选择器:匹配那些具有特定属性或属性值的元素 <style>/* */input[type"password"] {background-color: aqua;}/* 具有某个属性的指定标签 */div[title] {background-color: pink;}/* 属性的值中包含某个值 */inpu…...
Rust 函数体内能定义数据类型或者做其他什么事情吗?
一、可以在函数体内定义数据类型吗? 在 Rust 中,你不能直接在函数体内定义新的数据类型(如结构体或枚举)。数据类型必须在模块或块的作用域内定义,这通常是在函数外部。然而,你可以在函数体内定义新的类型…...

flask_restful结合蓝图使用
在蓝图中,如果使用 Flask_RESTful , 创建 Api 对象的时候,传入蓝图对象即可,不再是传入 app 对象 /user/__init__.py from flask.blueprints import Blueprintuser_bp Blueprint(user,__name__)from user import views /user…...

干货分享之反射笔记
入门级笔记-反射 一、利用反射破泛型集合二、Student类三、获取构造器的演示和使用1.getConstructors只能获取当前运行时类的被public修饰的构造器2.getDeclaredConstructors:获取运行时类的全部修饰符的构造器3.获取指定的构造器3.1得到空构造器3.2得到两个参数的有参构造器&a…...

使用小皮【phpstudy】运行Vue+MySql项目
现在的情况是我扒到了一个开源的项目,现在想要实现一下前端对应的功能,后端是完备的,但是需要调用数据库将数据跑起来,这里可以使用到MySql数据库,这里我还发现了一个比较好用的软件小皮【phpStudy】 官网 一 安装软件…...
局部静态变量实现单例模式,线程安全(推荐使用)c++11
class Singleton{ public:~Singleton();static Singleton& getInstance(){static Singleton instance;return instance; } private:Singleton(); };原因是C 11标准中新增了一个特性叫Magic Static:如果变量在初始化时,并发线程同时进入到static声明语…...

Machine Learning机器学习之决策树算法 Decision Tree(附Python代码)
目录 前言: 一、决策树思想 二、经典决策树算法 三、算法应用案列 基于Python 和 Scikit-learn 库实现决策树算法的简单示例代码,用于解决分类问题: 四、总结 算法 决策树算法应用: 决策树算法优缺点: 博主介绍&…...

Mybatis-Plus——09,代码自动生成器
代码自动生成器 一、先创建一个表二、创建一个类,配置代码生成器三、运行方法四、运行主方法,报错了。 一、先创建一个表 二、创建一个类,配置代码生成器 package com.gang;import com.baomidou.mybatisplus.annotation.DbType; import com.…...

Temu api接口 获取商品详情 数据采集
iDataRiver平台 https://www.idatariver.com/zh-cn/ 提供开箱即用的Temu电商数据采集API,供用户按需调用。 接口使用详情请参考Temu接口文档 接口列表 1. 获取商品详情 参数类型是否必填默认值示例值描述apikeystring是idr_***从控制台里复制apikeycountrystrin…...

安捷伦Agilent N1912A功率计
181/2461/8938产品概述: Keysight(原Agilent) N1912A P系列双通道功率计可提供峰值、峰均比、平均功率、上升时间、下降时间、最大功率值、最小功率值以及宽带信号的统计数据。 Keysight(原Agilent) N1912A P系列双通道功率计, 可提供峰值、峰均比、平均功率、上升…...

ES 进阶知识
索引Index 一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们…...

ChatGPT 对 ELT的理解
本文主要内容来自 ChatGPT 4.0 到底什么是 ETL?在数据库内部,把数据从 ODS 层加工成 DWD,再加工成 DWS,这个过程和 ETL 的关系是什么?带着这些问题,我问了一下 ChatGPT,总结如下。 数据在两个数…...

qt事件机制学习笔记
实现闹钟功能 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), speecher(new QTextToSpeech(this)) //给语音播报者实例化空间 {ui->setupUi(this); }Widget::~Widget() {delete …...

网红电商主播培养体系招聘管理制度孵化方案
【干货资料持续更新,以防走丢】 网红电商主播培养体系招聘管理制度孵化方案 部分资料预览 资料部分是网络整理,仅供学习参考。 共120页可编辑(完整资料包含以下内容) 目录 主播团队组建方案 让好主播主动留下 1. 好主播选拔标准…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...

软件工程 期末复习
瀑布模型:计划 螺旋模型:风险低 原型模型: 用户反馈 喷泉模型:代码复用 高内聚 低耦合:模块内部功能紧密 模块之间依赖程度小 高内聚:指的是一个模块内部的功能应该紧密相关。换句话说,一个模块应当只实现单一的功能…...

【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...