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. 好主播选拔标准…...

Android获取经纬度的最佳实现方式
Android中获取定位信息的方式有很多种,系统自带的LocationManager,以及第三方厂商提供的一些定位sdk,都能帮助我们获取当前经纬度,但第三方厂商一般都需要申请相关的key,且调用量高时,还会产生资费问题。这…...

芒果YOLOv8改进137:主干篇CSPNeXt,小目标检测专用,COCO数据集验证,协调参数量和计算量的均衡,即插即用 | 打造高性能检测
该专栏完整目录链接: 芒果YOLOv8深度改进教程 芒果专栏 本篇基于 CSPNeXt 的改进结构,改进源码教程 | 详情如下🥇 本博客 CSPNeXt 改进 适用于 YOLOv8 按步骤操作运行改进后的代码即可 即插即用 结构,博客包括改进所需的 核心结构代码 文件 重点:🔥🔥🔥YOLOv8 …...

【测试开发学习历程】认识Python + 安装Python
目录 1 认识 Python 1.1 Python 的起源 1.2 Python的组成 1.2.1 解释器 1.1.2 Python 的设计目标 1.1.3 Python 的设计哲学 1.2 为什么选择 Python 测试人员选择Python的理由 1.3 Python 特点 面向对象的思维方式 1.4 Python 的优缺点 1.4.1 优点 1.4.2 缺点 3. 安…...

webpack proxy工作原理?为什么能解决跨域?
一、是什么 webpack proxy,即webpack提供的代理服务 基本行为就是接收客户端发送的请求后转发给其他服务器 其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制) 想要实现代理首先需要一个中间服务器,webpac…...

ArkTS编写的HarmonyOS原生聊天UI框架
简介 ChatUI,是一个ArkTS编写的HarmonyOS原生聊天UI框架,提供了开箱即用的聊天对话组件。 下载安装 ohpm install changwei/chatuiOpenHarmony ohpm 环境配置等更多内容,请参考如何安装 OpenHarmony ohpm 包 接口和属性列表 接口列表 接…...

uni-app中web-view的使用
1. uni-app中web-view的使用 uni-app中的web-view是一个 web 浏览器组件,可以用来承载网页的容器,uni-app开发的app与web-view实现交互的方式相关简单,应用通过属性message绑定触发事件,然后在web-view的网页向应用 postMessage 触…...

前端跨域概念及解决方法
文章目录 前端跨域概念及解决方法什么是跨域跨域的解决方法JSONP跨域CORS简单请求 非简单请求 Nginx反向代理 前端跨域概念及解决方法 什么是跨域 同源指:两个页面域名、协议、端口均相同。 同源策略是浏览器的一个安全限制,跨域是由浏览器的同源策略造…...

Redis中的事务机制
Redis中的事务机制 概述。 事务表示一组动作,要么全部执行,要么全部不执行。例子如下。 Redis提供了简单的事务功能,讲一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束&#x…...

从零到一构建短链接系统(八)
1.git上传远程仓库(现在才想起来) git init git add . git commit -m "first commit" git remote add origin OLiyscxm/shortlink git push -u origin "master" 2.开发全局异常拦截器之后就可以简化UserController 拦截器可以…...

缺省和重载。引用——初识c++
. 个人主页:晓风飞 专栏:数据结构|Linux|C语言 路漫漫其修远兮,吾将上下而求索 文章目录 C输入&输出cout 和cin<<>> 缺省参数全缺省半缺省应用场景声明和定义分离的情况 函数重载1.参数的类型不同2.参数的个数不同3.参数的顺…...