【JavaEE初阶系列】——多线程案例三——定时器

目录

🚩定时器是什么

🚩标准库中的定时器

🚩自定义定时器

🎈构造Task类

📝相对时间和绝对时间

🎈构造MyTime类

📝队列空和队列不为空

📝wait(带参)解决消耗资源问题

📝为什么使用wait,不使用sleep

📝为什么使用PriorityQueue(),不使用PriorityBlockingQueue()

🚩测试

🚩带有解释的完整代码


🚩定时器是什么

日常开发常见组件。约定一个时间,时间到达之后,执行某个代码逻辑。定时器非常常见,尤其在进行网络通信的时候。比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器

当客户端发出请求之后,等待响应,如果服务器迟迟没有响应,该怎么办?网络世界是很复杂的,我们也不知道这个请求有没有发过去?响应有没有丢?

对于客户端来说,不能无限的等,需要有一个最大限度,到达最大限度的时候,是重新发一遍,还是彻底放弃,还是其他的方式........

在java的标准库中,都是有现成的定时器实现的


🚩标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
import java.util.Timer;
import java.util.TimerTask;public class Test {public static void main(String[] args) {Timer timer=new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2s后执行");}},2000);System.out.println("开始执行");}
}

这段程序就是创建一个定时器,然后提交一个2000s后执行的任务。

此处使用的是匿名内部类的写法,继承了TimerTask并且创建出了一个实例。这样的目的是为了重写run,通过run描述任务的详细情况。因为TimerTask是个抽象类本身是没有方法的,它存在的意义是继承,本个案例是实现接口Runnable里面的run方法。通过run方法描述任务的详细情况。

另一个参数是相对时间,当前安排的任务,啥时候执行,此处填写的时间,就是以当前时刻为基准,往后再推xxx的时间。

主线程执行schedule方法的时候,就是把这个任务放到timer对象中了,与此同时,timer里头也包含了一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了。仔细观察,可以发现,整个进程其实并没有结束,就是因为Timer内部的线程,阻止了进程结束。等下面实现定时器的时候,我们就可以知道为什么不结束了。


🚩自定义定时器

我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:

 Timer timer=new Timer();
  • Timer中需要有一个线程,扫描任务是否到时间,可以执行了。
  • 需要有一个数据结构,把所有的任务都保存起来。
  • 还需要创建一个类,通过类的对象来描述一个任务(至少要包含任务内容和时间)

创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个TimeTask对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityQueue比较合适。因为Timer中添加的这些任务,都是带有一个“时间”,一定是时间小的先执行,最先执行的就是时间最小的任务,如果时间最小的任务还没到时间,其他任务更不会执行了。(优先级队列,可以使用O(1)时间,来获取到时间最小的任务)。


🎈构造Task类

MyTask 类用于描述一个任务 ( 作为 Timer 的内部类 ). 里面包含一个 Runnable 对象和一个 time( 毫秒时间戳)

📝相对时间和绝对时间

//执行任务的时间(绝对时间)
private long time;

此时记录的是一个“绝对的时间"(完整的时间戳)。

  • 绝对时间:当前具体的时间
  • 相对时间:时间间隔

schedule方法里面的第二个参数是相对时间,为什么构造的时候记录绝对时间呢?

后续扫描线程的时候,如何判定当前这个任务是否要执行?

  • 获取到当前的时间戳  14:00
  • 再获取到任务要执行的时间戳 14:05
  • 对比俩个时间戳(时间没有到,不能执行)

当前的时间戳是利用到  System.currentTimeMillis()方法,现在我写的时间是20:02,此时时间戳转换成时间就是绝对时间。我们再扫描线程的时候,我们比较的是 当前的时间戳和我要执行的时候的绝对时间,所以我们最好是记录绝对时间。

    public MyTask(Runnable runnable,long delay){this.time=System.currentTimeMillis()+delay;this.runnable=runnable;}

构造方法中的俩个参数是 执行任务的对象以及相对时间。因为再调用schedule方法的时候传的第二个参数是相对时间。我们只需要将 相对时间+时间戳=绝对时间,就可以算出来。

比如现在是20:07的时候,调用的是schedule方法,执行delay是5分钟,那么再对应上面的 System.currentTimeMillis()就是14:00,delay是5分钟,那么绝对时间也就是要执行的时间是14:05分。


当我们创建好属性和获取当前的对象和时间的时候,我们再代码中还有一定的问题。

我们要想到,我们用优先级队列来保存任务,那么用到优先级队列,要求里面的元素务必是可以比较的。所以我们需要实现Comparable接口,继承compareTo方法。

  @Overridepublic int compareTo(MyTask o) {return (int)(this.time-o.time);}

由于返回类型是int类型,而时间戳返回类型是long类型,所以我们需要强制类型转换。

我们所创建的任务类里面的成员属性(任务对象和绝对时间)还有构造方法,以及比较方法。为下面的扫描线程铺垫。


🎈构造MyTime类

实现计时器,首先我们要创建一个优先级队列,里面包含的元素是执行的任务,然后实现schedule方法,再调用schedule方法的时候,就插入队列中,而里面的扫描线程应该放在构造方法中,因为我们再创建MyTime类的时候,线程就启动。

class MyTime{PriorityQueue<MyTask>queue=new PriorityQueue<>();public void schedule(Runnable runnable,long delay){queue.offer(new MyTask(runnable,delay));//插入队列中}public MyTime(){Thread thread=new Thread(()->{while(true){}});thread.start();//放在构造方法中,再创建对象的时候,就启用线程。}
}

继续完善代码,


📝队列空和队列不为空

如果队列是空的话,我们就要阻塞等待,等到再调用schedule后插入任务的时候,我们就可以继续执行。我们就会联想到wait()和notify()方法。要想要用wait()就需要加锁。但是我们再上一篇的 阻塞队列中我们知道

wait()再执行的时候要经过三个步骤。

  • 释放锁 ——》前提是先拿到锁
  • 等待通知
  • 通知到来之后,唤醒,重新获得锁

其实我们可以看到我们再创建对象的时候扫描线程启动,当我们调用schedule方法的时候,这也是一个线程再给队列添加元素,不同的线程,再对同一个队列操作,是肯定有线程安全问题的,所以不管使不使用wait()都是得加锁的。

 public void schedule(Runnable runnable,long delay){synchronized (this){queue.offer(new MyTask(runnable,delay));this.notify();//当插入任务后,我们就要唤醒线程}}public MyTime(){Thread thread=new Thread(()->{while(true){try {synchronized (this){while(queue.isEmpty()){this.wait();//如果队列为空,我们就要阻塞等待(队列插入任务)}}}catch (InterruptedException e){e.printStackTrace();}}});thread.start();}

如果队列不为空的情况下

首先我们需要获取当前堆顶的元素(也就是执行时间最先的一个),然后我们需要获取当前的绝对时间(就是你现在再看我的博客的时间),我们需要和任务类里面的time绝对时间(这里的绝对时间是我们要执行任务的时间)相比较,如果当前时间大于等于执行任务时间,那么我们就得执行该任务,执行完后从队列中删除,如果当前时间小于要执行任务的时间,我们就得不执行,等到时间到了为止。

class MyTime{PriorityQueue<MyTask>queue=new PriorityQueue<>();public void schedule(Runnable runnable,long delay){synchronized (this){queue.offer(new MyTask(runnable,delay));this.notify();}}public MyTime(){Thread thread=new Thread(()->{while(true){try {synchronized (this){while(queue.isEmpty()){this.wait();}MyTask myTask=queue.peek();//获取最先执行的任务(里面有任务对象和绝对时间)long currentTime=System.currentTimeMillis();//获得当前时间if(currentTime>=myTask.getTime()){myTask.getRunnable().run();//执行这个任务queue.poll();//删除堆顶元素}else{}}}catch (InterruptedException e){e.printStackTrace();}}});thread.start();}
}

📝wait(带参)解决消耗资源问题

程序到这里还是有点问题的。

这个程序到这里,比如,我队列中有一个任务,是14:30执行,当时时刻是14:00(时间未到),当时间没到的时候,此处的循环,会快速循环很多次,相当于,时间没到,但是我在这不停的看表,本来这个时间是可以休息的,但是这个不停看表的动作,使我并没有休息~同时,也没有干活。忙等,确实再等,但是也消耗了很多cpu资源,因为没有到时间依旧进入循环。所以此处也加个wait,这里的wait,引入带参数的版本(带有超时时间),把时间间隔作为wait的等待时间了,14:00——14:30 ,此时wait就直接等30min就可以了。

this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间

而且当任务时间没到的时候,就wiat阻塞(线程不会再cpu上调度,也就把cpu资源让出来给别人了)


📝为什么使用wait,不使用sleep

但是为啥使用wait,不使用sleep行不行? ——wait是更好的

可能在等待过程中,主线程调用schedule添加一个新的任务,新的任务是14:10执行,比刚才最早的任务还早,恰好使用刚才的schedule中的notify就可以唤醒这里的wait,让循环再执行一遍,重新拿到队首的元素(14:10),接下来wait的时间就更新成10min。

 if(currentTime>=myTask.getTime()){myTask.getRunnable().run();//执行这个任务queue.poll();//队列中执行完了,删除堆顶元素}else{//当前时间还没到任务时间,暂时不执行任务,但是先啥都不干,等待下一轮的循环判定this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间}

📝为什么使用PriorityQueue(),不使用PriorityBlockingQueue()

为什么使用priorityQueue(),其实就是因为要处理俩个wait地方,如果使用阻塞版本的优先级队列,不方便实现这样的俩处等待。

用priorityQueue优先级队列时,一个notify()可以用到俩个wait(),入完任务之后,队列不为空,就唤醒,当最新任务时间还没到的时候,进入阻塞,新的任务来了之后,就唤醒一下,然后重新判定一下最早的任务是啥以及更新等待时间。(所以notify可以同时唤醒俩个wait(),因为入完队列俩个都得唤醒)


🚩测试

public class Test_Timer2 {public static void main(String[] args) {MyTimer myTimer=new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("chenle");}},2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("zyf");}},3000);System.out.println("开始");}
}

因为没有任务的时候,代码会一直等待阻塞。 


🚩带有解释的完整代码


import java.util.PriorityQueue;//首先创建一个类,里面有对象和时间
class MyTask implements Comparable<MyTask>{private Runnable runnable;//执行任务的时间(绝对时间)private long time;public MyTask(Runnable runnable,long delay){this.time=System.currentTimeMillis()+delay;this.runnable=runnable;}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}@Overridepublic int compareTo(MyTask o) {return (int)(this.time-o.time);}
}class MyTime{PriorityQueue<MyTask>queue=new PriorityQueue<>();public void schedule(Runnable runnable,long delay){synchronized (this){queue.offer(new MyTask(runnable,delay));this.notify();}}public MyTime(){//扫描线程,需要不停的扫描,是否有任务没有完成Thread thread=new Thread(()->{while(true){try {synchronized (this){//不要使用if,作为wait的判断条件,应该使用while//使用while的目的是为了在wait唤醒的时候,再确认一下条件if(queue.isEmpty()){//使用wait进行等待//这里的wait,需要有另外的线程唤醒//添加了新的任务,就应该唤醒this.wait();}MyTask myTask=queue.peek();//获取最先执行的任务(里面有任务对象和绝对时间)long currentTime=System.currentTimeMillis();//获得当前时间//比较一下看当前的队首元素是否可以执行了if(currentTime>=myTask.getTime()){myTask.getRunnable().run();//执行这个任务queue.poll();//队列中执行完了,删除堆顶元素}else{//当前时间还没到任务时间,暂时不执行任务,但是先啥都不干,等待下一轮的循环判定this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间}}}catch (InterruptedException e){e.printStackTrace();}}});thread.start();}
}
public class Test_Timer2 {public static void main(String[] args) {MyTimer myTimer=new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("chenle");}},2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("zyf");}},3000);System.out.println("开始");}
}

如果你不在乎别人的态度,那么你的幸福就会变得无比简单。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/772635.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker配置镜像加速后容器和镜像消失

一、问题描述 根据阿里云给docker配置镜像加速器 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https://gt6j98xi.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl rest…

NO9 蓝桥杯单片机串口通信之进阶版

1 回顾 串口通信的代码编写结构还是与中断一样&#xff0c;不同的是&#xff1a; 初始中断函数条件涉及到串口通信相关的寄存器和定时器1相关的寄存器&#xff08;定时器1用于产生波特率&#xff09;&#xff0c;但初始条件中的中断寄存器只考虑串口通信而不考虑定时器1。 v…

基于springboot+vue+Mysql的网上图书商城

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

多功能的RSS工具RSS Funnel

什么是 RSS Funnel &#xff1f; RSS Funnel 是一个模块化的 RSS 处理管道系统。它可以以多种方式操作 RSS/Atom 源。 例如&#xff1a; 提取完整内容从 HTML 页面生成 RSS 订阅删除不需要的元素和文本使用正则表达式进行文字编辑或替换保留或删除与关键词或模式匹配的文章突出…

2024年【熔化焊接与热切割】考试报名及熔化焊接与热切割找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割考试报名考前必练&#xff01;安全生产模拟考试一点通每个月更新熔化焊接与热切割找解析题目及答案&#xff01;多做几遍&#xff0c;其实通过熔化焊接与热切割实操考试视频很简单。 1、【单选题】 下…

SE注意力模块学习笔记《Squeeze-and-Excitation Networks》

Squeeze-and-Excitation Networks 摘要引言什么是全局平均池化&#xff1f; 相关工作Deep architectures Squeeze-and-Excitation Blocks3.1. Squeeze: Global Information Embedding3.2. Excitation: Adaptive Recalibration3.3. Exemplars: SE-Inception and SE-ResNet 5. Im…

2024.3.26学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p273-p285 包 包的本质实际上就是创建不同的文件夹/目录来保存类文件 包的三大作用 区分相同名字的类 当类很多时&#xff0c;可以很好的管理类 控制访问范围 包的基本语法 package com.xx…

出席2024亚太内容分发大会,火山引擎边缘云“加速”游戏体验升级

3月26日&#xff0c;第十四届亚太内容分发大会暨CDN峰会在北京成功举办&#xff0c;火山引擎边缘云产品架构高级总监许思安出席并以《火山引擎边缘云游戏行业解决方案&#xff0c;“加速”游戏体验升级》为主题&#xff0c;分享了火山引擎边缘云在游戏行业的思考和实践。同时&a…

面试经典150题【91-100】

文章目录 面试经典150题【91-100】70.爬楼梯198.打家劫舍139.单词拆分322.零钱兑换300.递增最长子序列77.组合46.全排列39.组合总和&#xff08;※&#xff09;22.括号生成79.单词搜索 面试经典150题【91-100】 五道一维dp题五道回溯题。 70.爬楼梯 从递归到动态规划 public …

idea中Git项目遇到“Filename too long”错误 与 配置Git的ssh证书

一&#xff1a;“Filename too long”问题解决办法 错误信息&#xff1a; fatal: cannot create directory at xxxx: Filename too long warning: Clone succeeded, but checkout failed. You can inspect what was checked out with git status and retry with git restore …

24/03/26总结

面向对象练习题&#xff1a;&#xff08;封装&#xff0c;继承&#xff0c;多态) 封装&#xff1a;对象代表什么&#xff0c;就得封装对应的数据&#xff0c;并提供数据对应的行为,(把零散的数据和行为封装成一个整体&#xff1a;也就是我们说的对象&#xff09; 继承:当封装…

latex中的算法algorithm报错Undefined control sequence.

这里写目录标题 1. 错误原因2. 进行改正3. 爱思唯尔期刊与施普林格期刊对于算法的格式不太一样&#xff0c;不能直接套用总结 1. 错误原因 我在算法中使用\State 2. 进行改正 换成\STATE 3. 爱思唯尔期刊与施普林格期刊对于算法的格式不太一样&#xff0c;不能直接套用 总…

多线程执行一半后不往后走的坑

场景简单演示 首先演示一个简单的场景。 采用ThreadPoolExecutor提交线程的方式&#xff0c;直接在多线程中执行的某个地方抛出一个异常。 用submit方法提交的情况&#xff1a; 调用的地方&#xff1a; 发现一直卡在那&#xff0c;没有任何错误日志。 改成execute方法提交多…

优秀电源工程师需要的必备技能

随着电源市场的不断扩张,开关电源行业飞速发展,企业对电源工程师的需求日益增加,对电源工程师的技能要求也日渐提高,相信没有一位电源工程师会错过让自己变得更优秀的机会。作为一名数字电源从业者,今天就带大家细数一下优秀电源工程师具备的那些技能。 一、新手必备课程…

#GIT|Git Flow#Gitflow工作流程

Gitflow是一种使用功能分支和多个主分支的Git分支模型&#xff0c;它适用于有预定发布周期的项目&#xff0c;也适用于DevOps最佳实践中的持续交付。这个工作流程不会添加任何新的概念或命令&#xff0c;而是为不同的分支分配了非常具体的角色&#xff0c;并定义了它们应该如何…

串口通信标准RS232 RS485 RS422的区别

RS-232、RS-422、RS-485是关于串口通讯的一个机械和电气接口标准&#xff08;顶多是网络协议中的物理层&#xff09;&#xff0c;不是通讯协议&#xff0c;它们之间的几个不同点如下&#xff1a; 一、硬件管脚接口定义不同 二、工作方式不同 RS232&#xff1a; 3线全双工 RS…

干货分享之反射笔记

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

Vue2(十一):全局事件总线、消息订阅与发布pubsub、TodoList的编辑功能、$nextTick、过渡与动画

一、全局事件总线 1、思路解析 一种组件间通信的方式&#xff0c;适用于任意组件间通信。通俗理解就是一个定义在所有组件之外的公共x&#xff0c;这个x可以有vm或vc上的同款$on、$off、$emit&#xff0c;也可以让所有组件都访问到。 第一个问题&#xff1a;那怎样添加这个x才…

自省式RAG 与 LangGraph的实践

自省式 RAG 对实现 RAG 的步骤进行逻辑分析&#xff1a;比如&#xff0c;我们需要知道什么时候进行检索&#xff08;基于问题和索引的构成&#xff09;、何时改写问题以提升检索效率&#xff0c;或者何时抛弃无关的检索结果并重新检索。因此提出了自省式 RAG&#xff0c;自省式…

[医学分割大模型系列] (3) SAM-Med3D 分割大模型详解

[医学分割大模型系列] -3- SAM-Med3D 分割大模型解析 1. 特点2. 背景3. 训练数据集3.1 数据集收集3.2 数据清洗3.3 模型微调数据集 4. 模型结构4.1 3D Image Encoder4.2 3D Prompt Encoder4.3 3D mask Decoder4.4 模型权重 5. 评估5.1 评估数据集5.2 Quantitative Evaluation5.…