多线程 - 定时器

v2-fba4ff60d8ab6684550fa59adcbee98b_b

多线程 - 定时器

定时器的背景知识

定时器 ~~ (就类似于定闹钟)

平时的闹钟,有两种风格:

  1. 指定特定时刻,提醒
  2. 指定特定时间段之后,提醒

这里的“定时器”,不是提醒,而是执行一个实现准备好的方法/代码,它是开发中一个常用的组件,尤其是在网络编程的时候,使用浏览器上网,打开一个网页,很容易出现,“卡了""连不上"的情况.这时就可以使用“定时器”来进行“止损”.

标准库提供的定时器

timer.schedule();这个方法的效果是,给定时器,注册一个任务.任务不会立即执行,而是在指定时间进行执行.

public static void main(String[] args) {System.out.println("程序启动!");// 这个 Timer 类就是标准库中的定时器Timer timer =new Timer();   timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("运行定时器任务");}},3000);
}

第一个参数: new TimerTask() => TimerTask这个抽象类实现了Runnable接口,即将要执行的任务代码 ~~ public abstract class TimerTask implements Runnable
第二个参数: 指定多长时间之后执行(单位为毫秒)

手动实现一个定时器

定时器要求:

  1. 让被注册的任务,能够在指定时间被执行.
  2. 一个定时器是可以注册N个任务的,N个任务会按照最初约定的时间,按顺序执行.

思路:

在指定时间被执行 => 单独在定时器内部,创建个线程,让这个线程周期性的扫描,判定任务是否是到时间了.如果到时间了,就执行.没到时间,就再等等.
注册N个任务 => 这个N个任务,就需要使用一个数据结构来保存的,而在当下场景中,使用优先级队列,就是一个很好的选择.再由于这里的每个任务都是需要按时间执行的,时间越靠前,就越先执行,时间小的,优先级就高.此时队首元素,就是整个队列中,最先要执行的任务 => 这时,扫描线程,只需要扫一下队首元素即可,就不必遍历整个队列(如果队首元素还没到执行时间内,后续元素更不可能到时间).

image-20231006160206998

问题:

问题一: 因为调用schedule是一个线程,扫描是另一个线程,这里的优先级队列就会在多线程环境下使用了,这时就不得不考虑线程安全了.
问题二: 队列中的任务如何表示? 使用Runnable来表示任务的话是不行的,Runnable只是表述了任务内容,还需要描述任务什么时候被执行.
问题三: 如何进行任务的注册/创建?
问题四: 扫描线程具体的实现?
问题五: 任务MyTask如何进行优先级的比较?

解决:

问题一: 使用标准库提供的带优先级的阻塞队列 PriorityBlockingQueue,它本身就是线程安全的,就不需要考虑了.
问题二: 自定义一个MyTask类,来表示一个定时器中的任务,这个类包含两个私有属性private Runnable runnable;private long time; ~~ runnable是要执行的任务内容,time是任务在什么时候执行(使用毫秒时间来表示).
问题三: 提供一个schedule方法,来进行任务的注册/创建,这个schedule方法本身是比较的简单的,只是单纯的把任务放到队列里.
问题四: 取出队首元素, 检查看看队首元素任务是否是到时间了,如果时间没到,把取出来的元素重新入队queue.put(myTask);,在 put 之后, 再进行一个 waitthis.wait(myTask.getTime() - curTime);,如果时间到了,就执行任务内容.
问题五: 1.明确当前的任务是怎样的优先级,以哪个字段/属性指定优先级关系.2.让MyTask类实现Comparable接口,或者使用Comparator单独写个比较器(博主选择的是实现Comparable接口).

优化: Timer 类中存在一个 worker 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务设定的时间已经到达了,相关代码如下:

while (true) {     try {synchronized (this) {MyTask myTask = queue.take();long curTime = System.currentTimeMillis();if (curTime < myTask.getTime()) {// 还没到时间,先不必执行queue.put(myTask);} else {// 时间到了,执行任务myTask.run();}}} catch (InterruptedException e) {throw new RuntimeException(e);}
}

但是当前这个代码中存在一个严重的问题, 就是 while (true) {queue.put(myTask);}假设现在是8:00,队首元素的任务是10:00,取出的元素,显然是不能执行的,而由于这里的队列是优先级队列(堆),queue.put(myTask)会触发优先级调整,(堆的调整)调整之后, myTask 又回到队首了,下次循环取出来的还是这个任务. => 它就是一个没有任何阻塞的循环,在8:00到10:00这个时间段内,这个循环可能就要执行数以十亿次….就会造成了无意义的CPU浪费.

理解: 好比我们上高中的时间,每天都要6:00起床,而我有次5:00就醒了,看了眼闹钟,发现是5:00,正常来说,我会立刻继续睡,再睡个半小时,但是这个代码却不是这样的,按着这个代码执行逻辑的,我就必须在放下表后,又立刻拿起表来,又看时间,发现是5:00,然后又拿起闹钟,看时间,就这样重复着,知道时间到了6:00,然后才起床上学,但是这个一看不科学啊!这样做,就毫无意义,这样的代码是存在问题滴!!!

这种现象,在我们计算机领域也被称为“忙等” ~~ 等,但并没有闲着.正常来说,等待是要释放CPU资源的,让CPU做其它的事情,但是“忙等”,既进行了等待,又占用着CPU资源.
注: 像忙等这样的情况,也是需要辩证的看待的.在当前场景中,”忙等”,确实是不太好的.但是有的情况下,忙等,却是一个好的选择.

策略: 针对上述代码,就不要进行“忙等”了,而是进行"阻塞式"等待.这时就想到sleep或者wait,不过,博主要说的是sleep看似可行,但是实际上不可以的,因为做不到等待的时间明确!!!随时都可能会有新的任务创建/注册(随时可能有线程调用schedule添加新任务),万一新的任务更早了,是做不到等待时间的更新,此时仍然按照之前的等待,就会错过新任务的执行时间. 使用wait更合适,更方便随时唤醒.使用wait等待,每次有新任务来了(有线程调用schedule),就 notify一下,重新检查下时间.并再次计算要等待的时间,从而做到等待时间的更新.
注: 这里的wait是要使用带有“超时时间”版本的,这样就可以保证: 1.当新任务来了,随时 notify 唤醒; 2.如果没有新任务,则最多等到之前旧任务中的最早任务时间到,就被唤醒.


高能烧脑预警

博主代码写的过程中,遇到的一个线程安全/随机调度密切相关的问题.
考虑一个极端情况:
image-20231006221842050

看了上述图示之后,就不难发现,问题出现的原因,是因为当前 take 操作,和 wait 操作,并非是原子的.如果在 take 和 wait 之间加上锁,保证在这个过程中,不会有新的任务过来,问题自然解决(换句话说,只要保证每次 notify 时,确实都正在wait ) => 扩大上述代码锁的范围.

image-20231006223301553

代码编写:

package thread;import java.util.concurrent.PriorityBlockingQueue;/*** Created with IntelliJ IDEA.* Description:* User: fly(逐梦者)* Date: 2023-10-06* Time: 16:32*/// 使用这个类来表示一个定时器的任务.
class MyTask implements Comparable<MyTask> {// 要执行的任务内容private Runnable runnable;// 任务在什么时候执行(使用毫秒时间来表示)private long time;public MyTask(Runnable runnable, long time) {this.runnable = runnable;this.time = time;}// 获取当前任务时间public long getTime() {return time;}// 执行任务public void run() {runnable.run();}@Overridepublic int compareTo(MyTask o) {// 返回 小于 0, 大于 0, 0// this 比 o 小, 返回 < 0// this 比 o 大, 返回 > 0// this 和 o 相同, 返回 0// 当前要实现的效果, 是队首元素为时间最小的任务return (int) (this.time - o.time);}
}// 自己写个简单的定时器
class MyTimer {// 扫描线程private Thread t = null;public MyTimer() {t = new Thread(() -> {while (true) {// 取出队首元素, 检查看看队首元素任务是否是到时间了// 如果时间没到,把取出来的元素重新入队// 如果时间到了,就把任务进行执行try {synchronized (this) {MyTask myTask = queue.take();long curTime = System.currentTimeMillis();if (curTime < myTask.getTime()) {// 还没到时间,先不必执行// 现在是13:00,取出来的任务是14:00 执行queue.put(myTask);// 在 put 之后, 再进行一个 waitthis.wait(myTask.getTime() - curTime);} else {// 时间到了,执行任务myTask.run();}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();}// 用一个阻塞优先级队列, 来保存任务private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();// 指定两个参数// 第一个参数是 任务内容// 第二个参数是 任务在多少毫米之后执行. 形如 1000public void schedule(Runnable runnable, long after) {// 进行时间上的换算MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);queue.put(task);synchronized (this) {this.notify();}}// 这个 schedule 方法本身比较简单,只是单纯的把任务放到队列里去了
}public class ThreadDemo25 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("任务1");}}, 1000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("任务2");}}, 2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("任务3");}}, 3000);}
}

博主备注: 程序里的计时操作,本身就难以做到非常精确,因为操作系统调度线程有时间开销的.存在ms级别的误差,都很正常.也不影响日常使用.如果应用场景,就是对时间误差非常敏感(发射导弹,发射卫星)此时就不会再使用windows, linux这样的操作系统了,而应该使用像vxworks 这样的实时操作系统,这样的系统线程调度开销是极快,可控的,可以保证误差在要求范围内的.

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

亚马逊计划向开创性的人工智能初创公司Anthropic投资高达4亿美元

原创 | 文 BFT机器人 在一项巨大而突破性的举措中&#xff0c;亚马逊公布了向人工智能初创公司Anthropic投资高达4亿美元的计划&#xff0c;其愿景是创建更易于理解和可控的人工智能系统。此次合作标志着亚马逊打算在人工智能领域率先取得进步&#xff0c;巩固其在技术领域的地…

【QT5-程序控制电源-[GPIB-USB-HS]-SCPI协议-上位机-基础样例【2】】

【QT5-程序控制电源-[GPIB-USB-HS]-SCPI协议-上位机-基础样例【2】】 1、前言2、实验环境3、自我总结1、基础了解仪器控制-熟悉仪器2、连接SCPI协议3、了解GPIB-USB-HS4、软件调试-代码编写 4、熟悉协议-SCPI协议5、实验过程-熟悉软件&#xff08;1&#xff09;去官网NI&#x…

玩转Linux—如何在Linux环境中部署MySQL、Redis和nginx

1、Linux常用命令 Linux学习之路&#xff1a; VMware虚拟机安装Linux系统(详解版) 查看当前文件目录&#xff1a;ls查看目录中文件详细信息&#xff1a;ll输出当前所处的目文件目录&#xff1a;pwdLinux查看当前IP地址&#xff1a;ifconfigWindows查看当前IP地址&#xff1…

MS31703H 桥栅极驱动控制器,可P2P替代TI的DRV8703

MS31703NA 是一款小型单通道 H 桥栅极驱动 器。它使用四个外部 N 通道 MOSFET &#xff0c;驱动一个双 向刷式直流电机。 PH/EN 、独立半桥或 PWM 允许轻松连接到控制 器电路。内部传感放大器提供可调的电流控制。集 成的电荷泵可提供 100% 占空比&#xff0c;而…

机械臂运动控制,通讯的解包->运动控制->数据封包上报过程

一、协议 数据格式为小端模式&#xff0c;浮点数格式为IEEE754&#xff0c;需与上位机的PC端一致&#xff0c;如window系统&#xff0c;其它系统需要自行测试&#xff0c;用于传输16位、32位、float数据格式&#xff0c;避免只传输字节数据带来转换的繁琐及精度丢失。 二、下位…

机器学习:随机森林

集成学习 集成学习&#xff08;Ensemble Learning&#xff09;是一种机器学习方法&#xff0c;通过将多个基本学习算法的预测结果进行组合&#xff0c;以获得更好的预测性能。集成学习的基本思想是通过结合多个弱分类器或回归器的预测结果&#xff0c;来构建一个更强大的集成模…

springboot+jsp+ssm高校图书馆图书借阅收藏评论管理系统617w1

本图书管理系统系统采用B/S架构&#xff0c;数据库是MySQL&#xff0c;网站的搭建与开发采用了先进的Java进行编写&#xff0c;使用了SSM&#xff08;Spring、SpringMVC、Mybits&#xff09;框架。该系统从两个对象&#xff1a;由管理员和用户来对系统进行设计构建。前台主要功…

面试题:在大型分布式系统中,给你一条 SQL,让你优化,你会怎么做?

亲爱的小伙伴们&#xff0c;大家好呀&#xff01;我是小米&#xff0c;一个热爱技术、乐于分享的90后程序猿。今天&#xff0c;我要和大家聊聊一个在大型分布式系统中非常有趣和挑战性的话题——如何优化 SQL 查询&#xff01; 这个问题可不简单&#xff0c;但不要担心&#x…

python练习4

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

Vue3最佳实践 第七章 TypeScript 创建Trello 任务管理器

| ​ 我们将探讨如何使用Vue.js从零开始创建一个类似于Trello的任务管理应用程序。如果你不熟悉Trello&#xff0c;它是一款非常流行的任务管理工具&#xff0c;允许你把任务写在卡片上&#xff0c;然后通过一个看板的方式来直观地管理这些任务。Trello不仅可以用于个人的任务…

电子地图 | VINS-FUSION | 小觅相机D系列

目录 一、相关介绍 二、VINS-FUSION环境安装及使用 &#xff08;一&#xff09;Ubuntu18.04安装配置 1、Ubuntu下载安装 2、设置虚拟内存&#xff08;可选&#xff09; &#xff08;二&#xff09;VINS-FUSION环境配置 1、ros安装 2、ceres-solver安装 3、vins-fusion…

JavaScript中的map()和forEach()方法有什么区别?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

工信部教考中心:什么是《研发效能(DevOps)工程师》认证,拿到证书之后有什么作用!(上篇)丨IDCF

在计算机行业中&#xff0c;资质认证可以证明在该领域内的专业能力和知识水平。各种技术水平认证也是层出不穷&#xff0c;而考取具有公信力和权威性的认证是从业者的首选。同时&#xff0c;随着国内企业技术实力的提升和国家对于自主可控的重视程度不断提高&#xff0c;国产证…

基于Java的教学评价管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言系统功能结构图系统ER图具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划…

Flink+Doris 实时数仓

Flink+Doris 实时数仓 Doris基本原理 Doris基本架构非常简单,只有FE(Frontend)、BE(Backend)两种角色,不依赖任何外部组件,对部署和运维非常友好。架构图如下 可以 看到Doris 的数仓架构十分简洁,不依赖 Hadoop 生态组件,构建及运维成本较低。 FE(Frontend)以 Java 语…

用 Pytorch 自己构建一个Transformer

一、说明 用pytorch自己构建一个transformer并不是难事,本篇使用pytorch随机生成五千个32位数的词向量做为源语言词表,再生成五千个32位数的词向量做为目标语言词表,让它们模拟翻译过程,transformer全部用pytorch实现,具备一定实战意义。 二、论文和概要 …

【数据结构--八大排序】之希尔排序

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

STM32--人体红外感应开关

本文主要介绍基于STM32F103C8T6和人体红外感应开关实现的控制算法 简介 人体红外模块选用HC-SR501人体红外传感器&#xff0c;人体红外感应的主要器件为人体热释电红外传感器。人体都有恒定的体温&#xff0c;一般在36~37度&#xff0c;所以会发出特定波长的红外线&#xff0…

Mac上protobuf环境构建-java

参考文献 getting-started 官网pb java介绍 maven protobuf插件 简单入门1 简单入门2 1. protoc编译器下载安装 https://github.com/protocolbuffers/protobuf/releases?page10 放入.zshrc中配置环境变量  ~/IdeaProjects/test2/ protoc --version libprotoc 3.12.1  …

国庆假期作业6

一、ARM的工作模式 1、非特权模式 user模式&#xff1a;非特权模式&#xff0c;大部分任务执行在这种模式 2、特权模式 异常模式&#xff1a; FIQ : 当一个快速&#xff08;fast) 中断产生时将会进入这种模式 IRQ : 当一个通用&#xff08;normal) 中断产生时将会进入这种模式…