【多线程】深入剖析定时器的应用

news2024/11/13 9:26:51

85bef0f9be7b443ebfd3dbba0de889c1.png

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


        在软件开发中,有一些代码逻辑并不需要立马就被执行,可能需要等一段时间在执行。就好像我们会用闹钟来提醒我们过一段时间后要做某事一样,代码中也有“定时器”这种类似于闹钟的机制。那么,计时器都有什么特点,又该如何使用呢,就让博主手把手带你研究一下吧。

一、什么是定时器

定时器也是软件开发中的⼀个重要组件. 类似于⼀个 "闹钟".使其能够在未来的某个时间点或按照预定的时间间隔执行某个指定好的代码。是⼀种实际开发中⾮常常⽤的组件。

4658a6c4cf8b4267ae1e1b11d4118a15.png

常见作用:

1. 计划任务:定时器可以用于执行定期任务,比如数据备份、日志清理、定时发送邮件等。

2. 延迟操作:可以设置一个任务在一段时间后执行,例如,实现一个倒计时功能,在特定时间后触发事件。

3. 定时刷新:在Web应用中,定时器可以用于定时刷新页面或数据,保持与服务器同步的状态。

4. 心跳检测:在网络通信中,定时器可以用于定期发送心跳包,以保持连接活动状态,防止超时断开。

5. 自动更新:可以设置定时器来检查是否有软件更新,如果有的话就自动下载更新包。

6. 提醒服务:在个人应用中,如闹钟、日历应用等,定时器可以用来设置提醒。

7. 资源监控:定时器可以用于定期监控系统资源使用情况,如CPU、内存使用率,然后根据这些信息做出相应的资源管理决策。

8. 性能测试:在软件测试中,可以使用定时器来模拟用户行为,定期发送请求,从而测试系统的响应时间和负载能力。

9. 计时功能:在游戏开发或其他应用中,定时器可用于控制游戏时间或执行计时相关的逻辑。

10. 自动化脚本:定时器可以用于执行批处理脚本或自动化任务,比如定期清理临时文件、统计日志等。

11. 状态维护:某些网络协议要求客户端定期向服务器发送消息来维持连接状态,定时器可以帮助实现这种功能。

二、定时器的实现

实现要点:

1、创建类,描述要执行的任务是啥

2、管理多个任务,通过一定的数据结构,把多个任务存起来

3、用专门的线程去执行这些任务

1、描述任务

我们先创建一个MyTimerTask类,来负责描述所执行的任务

class MyTimerTask{
    private Runnable runnable;
    //通过毫秒时间戳,表示这个任务具体执行时间
    private long time;

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

    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }
}

注意:

1、通过runnable 成员变量来存储执行的任务

2、通过成员变量 time 来存储任务执行的时间。这里用毫秒时间戳来表示,方便程序进行判断

3、构造方法中的delay表示定时器所定时长,并将time赋值为系统当前毫秒时间戳与delay之和,从而让程序能更精准地确定任务所应执行的时刻。

2、管理任务

存储在定时器中的各个任务一定是执行时间越小越早执行。因此我们每次都会取出任务管理中 time 值最小的任务,将其与当前时间戳进行比较,如果小于等于当前时间戳则开始执行。

这样我们就是不断地在取出当前管理的时间戳最小的任务,显然用 优先级队列 来存储会比较合适。优先级队列就能保证每次取出的元素都是最小(大)的,且取出元素的效率比较高,因此我们可以初步实现一个MyTimer类来通过优先级队列来管理任务MyTimerTask:

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
}

不过,PriorityQueue类会涉及到大小的比较,因此我们需要让MyTimerTask类重写Comparable接口:

8054e9c3f63c476587d10b3b7053ffe0.png

3、执行任务

在MyTimer内中实现不断取出堆顶元素并与当前时间戳比较,若当前时间戳以大于等于设定时间则执行,否则则不执行:

b7f9eb4367084d19af09e0c4d9584d31.png

再写一个schedule方法来负责往堆中加入新的元素:

f8e52774fb424df398ba3ac573efbadb.png

注意:由于这里涉及了大量的修改操作,为了避免线程安全问题,所以都为这两个操作加上了锁。对于线程安全问题的详细研究可以看一下博主的 深入剖析线程安全问题 一文

不过上述代码其实是存在很大问题的,比如:

(1)当队列中没有元素时:

b00a851d430a491d9c38da5b147450b4.png

该处的逻辑就会在短时间内进行大量地循环,会导致浪费许多系统资源来完成这种无意义的循环。类似于“线程饿死”的情况。

这时我们可以让线程在发现队列为空时就进入 wait 状态,等到调用schedule方法往队列中插入新元素时在调用notify让线程恢复运行即可。关于 wait 与notify的深入研究可以看看博主的 线程的等待与通知机制 。

d33fbd523f6b499f84b8688fa76f7996.png

a8071186f8ad4e66832615129a53dea4.png

(2)队列中已存在元素,但执行时间最小的任务与当前时间仍有一定时间间隔

ad804d9492a24bd7b6b90f327676d55c.png

比如当前时间为9:00,任务时间为11:00,这两个小时内程序就会不断进行上图的循环,也会浪费很多资源。同样的,我们也可以通过wait来解决这一问题:

222679fff45442c2951dc4bf3d718bd5.png

这里的wait与前面不同,不会指望schedule来唤醒。而是应该设置等待时间为任务执行时间与当前时间的差值,这样能保证刚好等待到最小执行任务需要执行的时间,然后重新恢复执行。

注意:这里的wait千万不能换成sleep

1、在等待的过程中,可能会调用 schedule 插入了一个执行时间更小的任务,如果是wait的话,就会被唤醒,重新设定等待时间。而sleep则会死等,中途插入新的任务可能就无法及时执行了

2、sleep休眠时不会释放锁,期间调用schedule方法就无法拿到锁,也就没有办法添加新的元素了

完整代码:

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;
    //通过毫秒时间戳,表示这个任务具体执行时间
    private long time;

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

    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }
    @Override
    public int compareTo(MyTimerTask o) {
        //此处决定了是大堆还是小堆
        return (int) (this.time-o.time);
    }

}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    public static Object lock=new Object();
    public MyTimer(){
        // 创建线程, 负责执行上述队列中的内容
        Thread t=new Thread(()->{
            while (true){
                synchronized (lock){
                    while(queue.isEmpty()){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask current=queue.peek();
                    if(System.currentTimeMillis()>=current.getTime()){
                        //执行任务
                        current.run();
                        //把执行过的任务从队列中删除
                        queue.poll();
                    }else {
                        //先不执行任务
                        try {
                            lock.wait(current.getTime()-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        t.start();
    }
    public void schedule(Runnable runnable,long delay){
        synchronized (lock){
            MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
            queue.offer(myTimerTask);
            lock.notify();
        }
    }

}

三、java标准库中的定时器

标准库中提供了⼀个 Timer 类,可以用来调度定时任务
基本用法:
1、创建Timer实例:
Timer timer = new Timer();

2、创建TimerTask实例:

TimerTask 是一个抽象类,用户需要继承这个类并重写其中的 run() 方法来定义任务的具体行为。

class MyTask extends TimerTask {
    @Override
    public void run() {
        // 在这里写入任务代码
    }
}

3、调度任务:

  • 使用 schedule(TimerTask task, long delay) 方法来安排一个任务在指定的延迟后运行一次。
  • 使用 schedule(TimerTask task, long delay, long period) 方法来安排一个任务在首次延迟后运行,并且之后每隔一个固定的时间间隔重复执行。
Timer timer = new Timer();
MyTask myTask = new MyTask();
timer.schedule(myTask, 1000); // 延迟1秒后执行一次
timer.schedule(myTask, 1000, 60000); // 延迟1秒后开始,每分钟执行一次

4、取消任务:

如果不再需要执行某个任务,可以通过调用 TimerTaskcancel() 方法来取消它

myTask.cancel(); // 取消任务

5、停止Timer

如果要停止整个定时器,可以调用 Timercancel() 方法。

timer.cancel(); // 停止定时器

注意:

  • Timer 和 TimerTask 并不是线程安全的,如果多个线程同时访问同一个 Timer 或者 TimerTask 实例,可能会导致未定义的行为。
  • Timer 创建的线程默认是非守护线程,这意味着如果应用的其他所有非守护线程都结束了,JVM不会自动退出。如果你希望你的应用程序能够在所有工作线程结束后立即退出,你应该考虑将 Timer 设置为守护线程。
  • Timer 是单线程的,所以如果你有多个任务并且其中一个任务执行时间过长,那么它会阻塞其他的任务。

那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

49b32cfb995a4f01a40b20f0b8a53579.png

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

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

相关文章

电脑文件怎么加密?文件加密方法介绍

随着数字化时代的到来&#xff0c;电脑文件的安全性问题日益凸显。为了保护个人隐私和企业数据&#xff0c;文件加密成为了一项重要的安全措施。本文将详细介绍几种常见的电脑文件加密方法&#xff0c;帮助你更好地保护自己的数据安全。 超级加密3000 超级加密3000提供了两种主…

哪款骨传导耳机适合运动?健身党无广安利五款有用的骨传导耳机!

作为一名耳机爱好者&#xff0c;我的耳机收藏可以说是丰富多样&#xff0c;从追求极致音质的头戴式&#xff0c;到便于携带的入耳式&#xff0c;再到近年来兴起的骨传导耳机&#xff0c;我都有所体验。在众多选择中&#xff0c;我最终偏爱上了骨传导耳机&#xff0c;它以其独特…

【Cadence26】无原理图直接绘制PCB项目的问题总结

【转载】Cadence Design Entry HDL 使用教程 【Cadence01】Cadence PCB Edit相对延迟与绝对延迟的显示问题 【Cadence02】Allegro引脚焊盘Pin设置为透明 【Cadence03】cadence不小心删掉钢网层怎么办&#xff1f; 【Cadence04】一般情况下Allegro PCB设计时的约束规则设置&a…

I-RNTI是什么?

I-RNTI是Inactive RNTI的缩写&#xff0c;它是per RNA配置的一个参数, 主要作用就是UE在RRCResume的时候, 方便new gNB去获取UE之前的锚点gNB(从而获取UE上下文)。 在R2-1812504中有关I-RNTI的agreement如上图&#xff1a; 1 gNB 在suspend消息中为 UE 配置full I-RNTI 和shor…

AI网盘搜索 1.2.6 智能文件搜索助手,一键搜索所有资源

对于经常需要处理大量文件的人来说&#xff0c;AI网盘检索简直是救星。它提供了智能对话式搜索功能&#xff0c;只需用自然语言描述就能找到需要的文件。此外&#xff0c;它还广泛支持各种文件类型&#xff0c;从文档到图片&#xff0c;全面覆盖。精准定位功能让您能够快速找到…

在对接电影票API时如何快速进行错误处理和调试

在对接电影票API时&#xff0c;进行有效的错误处理和调试是确保用户体验和系统稳定性的关键。以下是一些步骤和建议&#xff1a; 1.阅读API文档&#xff1a; 在开始对接前&#xff0c;彻底理解API文档中关于错误处理的部分&#xff0c;了解可能返回的所有错误码和它们的含义。…

微信小程序 === 长列表性能优化

目录 怎么做到的&#xff1f; 环境准备 使用开发者工具调试 开始迁移 在真机上预览效果 配置 We 分析 AB 实验 快捷切换入口 如何识别当前页面是否使用 Skyline 滚动容器及其应用场景 长列表 ScrollView 的三种模式 列表模式 自定义模式 嵌套模式 可拖拽容器 对…

启明云端乐鑫代理商,乐鑫ESP32无线芯片方案,物联网设备WiFi联动控制

随着智能和远程技术的飞速发展&#xff0c;物联网(IoT)逐渐出现在我们生活的每一个角落。乐鑫以其创新的无线通信技术&#xff0c;正成为智能家居、工业自动化和医疗设备等领域的推动者。 无线WiFi芯片模组不仅提供了强大的数据处理能力&#xff0c;还赋予了设备以直观的交互方…

香橙派模型转换以及部署二

由于想更新RKNN-Tookl2的版本&#xff0c;重新做一下记录。 上一篇文章安装的是RKNN-Tookl2 v1.5.2&#xff1a; 香橙派转换模型以及在开发板上部署-CSDN博客 现在记录一下RKNN-Tookl2 v2.0.0beta0版本&#xff0c;使用起来更方便&#xff0c;且可使用的功能更多一些&#x…

容联云容犀Copilot&Agent入选《中国 AI Agent 产品罗盘》

近日&#xff0c;InfoQ研究中心推出《中国AI Agent应用研究报告》&#xff0c;并在报告中对现行的中国AI Agent产品进行梳理总结&#xff0c;并形成《中国AI Agent产品罗盘》。 作为“营销服”领域垂直类Agent&#xff0c;容联云容犀Copilot&#xff06;Agent入选2024中国AI A…

天地一体化物联网:挑战与机遇

这篇论文的标题是《Space-Terrestrial Integrated Internet of Things: Challenges and Opportunities》&#xff0c;作者包括Juan A. Fraire, Oana Iova, 和 Fabrice Valois。文章发表在2022年12月的IEEE Communications Magazine上。论文主要探讨了如何将卫星通信与物联网&am…

vue3中的实例

实例类型 Vue2&#xff1a;每个Vue应用都是new Vue创建的一个新实例&#xff0c;创建的时候将data作为property添加到响应式系统中 vue3&#xff1a;createApp创建一个Application Instance、应用实例用来注册全局内容&#xff0c;大多数方法支持链式调用&#xff0c;返回实例…

C++当中的多态(二)

(三&#xff09;final和override关键字 在继承和多态当中我们还会很经常看到这两个关键字final和override。这两个关键字的作用其实很简单。 final关键字字面意思上理解就是我们这个虚函数是最后一个虚函数&#xff0c;之后不能够被重写。所以我们以后想要定义一个虚函数只能够…

word文档的读入(7)

获取了标准答案和学生答案后&#xff0c;就可以计算每位同学的填空题分数啦。我们将分数累加到studentData字典里的scoreTwo键中。需要注意的是&#xff0c;使用这个键之前&#xff0c;必须先在第一个for循环里和第二个for循环外&#xff0c;对它进行初始化赋值为0&#xff0c;…

一文搞懂线程的生命周期以及状态

一. Java 线程生命周期概述 Java 中的线程生命周期主要分为以下五个状态&#xff1a; 新建状态&#xff08;NEW&#xff09;&#xff1a;线程被创建但尚未启动。可运行状态&#xff08;RUNNABLE&#xff09;&#xff1a;线程可以被操作系统调度执行。阻塞状态&#xff08;BLO…

如何投放Spotify广告:费用与关键考量

Spotify在2008年上市时&#xff0c;市场上已经充斥着各种竞争对手的音乐服务。这款音乐流媒体应用不仅打破了预期&#xff0c;还在180个市场上吸引了超过602百万用户&#xff0c;其中包括2.36亿订阅用户。现如今&#xff0c;它是全球最受欢迎的音频流媒体订阅服务。 Spotify广…

MySQL分组查询(DQL)

先看一下我的表内容和数据&#xff0c;再做接下来的例子和讲解1.分组函数的基本用法 select 字段列表 from 表名 [WHERE 条件] group by 分组字段名 [HAVING 分组后的过滤条件] 2.先声明where和having的区别 1.执行时机不同&#xff1a;where是分组之前进行过滤&#xff0c;…

❤Node08-Express-jwt身份认证

❤Node08-Express-jwt身份认证 1、token基本概念​ Session认证的局限性​ Session 认证机制需要配合Cookie才能实现。由于 Cookie 默认不支持跨域访问&#xff0c;所以&#xff0c;当涉及到前端跨域请求后端接口的时候&#xff0c;需要做很多额外的配置&#xff0c;才能实现…

特征值特征向量

正交矩阵 施密特正交化,本质上是正交​编辑投影定理的应用

再次进阶 舞台王者 第八季完美童模全球赛荣耀大使【李暖希】赛场秀场超燃合集!

7月20-23日&#xff0c;2024第八季完美童模全球总决赛在青岛圆满落幕。在盛大的颁奖典礼上&#xff0c;一位才能出众的少女——李暖希&#xff0c;迎来了她舞台生涯的璀璨时刻。 荣耀大使——李暖希&#xff0c;以璀璨童星之姿&#xff0c;优雅地踏上完美童模盛宴的绚丽舞台&am…