【多线程】模拟实现一个定时器

news2024/11/24 19:58:51

1. Java自带的定时器

相信大家都定过闹钟,在我上学有早八的时候,硬是要定三个闹钟才起得来,7:20,7:30,7:40,那么我们今天所要实现的定时器,就类似于闹钟,设定多长时间之后,要干某某事情...

定时器是一种实际开发中非常常用的组件,比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连等等

在 Java 标准库中,也给我们提供了一个定时器:Timer 类,这个类的核心方法是 schedule。

schedule 方法中包含了两个参数,第一个参数指定将要执行的代码,第二个参数指定多长时间之后执行,单位是毫秒。

public static void main(String[] args) {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("执行任务!");
        }
    }, 3000);
}

此时我们运行程序,就会在 3s 后打印 "执行任务!",这里有个点需要注意,我们描述任务使用的是 TimerTask 里的 run 方法,其实这个 TimerTask 里的 run 方法,跟 Runnable 的 run 方法是一模一样,因为源码实现中,TimerTask 类实现了 Runnable 这个接口,相当于多了一层封装!

当然我们也可以一次注册多个任务,这就好比列出一个清单一样:

这里我们注意,清单上的任务有很多,每个任务多久后执行的时间点也不一样,那我写清单的时候,可能先想到下午要干嘛,再想到早上要干嘛,先在清单上写下午干的活,再写早上要干的活,但是我执行的顺序,肯定是先执行早上的任务,在执行中午,下午...

这就好比我们注册任务一样,先注册一个 3s 后执行的任务,再注册一个 2s 后执行的任务,显然后注册的任务要先执行!

public static void main(String[] args) {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("3秒后的任务执行");
        }
    }, 3000);
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("2秒后的任务执行");
        }
    }, 2000);
}
/*
打印结果:
2秒后的任务执行
3秒后的任务执行
*/

有了上述对定时器的认识,这里我们就模拟实现一个 Timer(定时器)。


2. 模拟实现定时器

上述的案例和分析,我们能很清楚的认识到,并不是先注册的任务先执行,而是按照时间前后来执行,比如我们定了三个闹钟:17:30 14:00 16:00,那么肯定是 14:00 的闹钟最先响!

所以我们注册的任务也是同理,是带有优先级的!这个优先级取决你设定的时间,到了时间就启动。

想到优先级,在前面学习的数据结构中,有一个优先级队列 PriorityQueue,底层是用堆来实现的,这样一来我们可以建小堆,按照指定的时间进行比较,谁会最先执行,谁就是堆顶的元素。

同时我们在内部定义一个线程,来扫描堆中元素是否到点该执行了,由于我们是小堆,所以堆顶元素一定是最先执行的,如果堆顶的任务都不能执行,那么后面的任务肯定也都不能执行,所以这个线程只需要扫描堆顶的元素,判断堆顶元素是否到时间的就行!

但是问题又来了,调用 schedule 注册任务时是一个线程往堆中写,而 MyTimer 内部还有一个线程一直读堆顶元素,而这两个线程都是在操作里面的优先级队列,势必会有线程安全问题(一个线程读,一个线程写),此处显然 PriorityQueue 就不行了,但是还有另外一个选择:PriorityBlockingQueue,这个上节我们提到过,是 Java 标准库提供的一个优先级阻塞队列,是线程安全的!

好了,基于上述的分析,下面我们就来模拟实现一个定时器,取名为 MyTimer:

这里我们使用内部类的方式,利用 MyTask 类来描述任务和执行任务的时间:

public class MyTimer {
    private static class MyTask implements Comparable<MyTask> {
        // 要执行的任务
        private Runnable runnable;
        // 执行任务的时间
        private long delay;

        private MyTask(Runnable runnable, long delay) {
            this.runnable = runnable;
            this.delay = delay;
        }

        private void run() {
            runnable.run();
        }

        // 重写 compareTo 按照执行时间进行比较
        @Override
        public int compareTo(MyTask o) {
            // 根据注册时间指定优先级, 建小堆
            return (int) (this.delay - o.delay);
        }
    }
}

接着 MyTimer 类中还需要有一个优先级阻塞队列来存放要执行的任务,加上一个线程来扫描堆顶元素:

// 存放任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

// 扫描线程
private Thread t;

那么这个 t 线程如何扫描呢?此处我们可以在 MyTimer 构造方法中,让 t 线程进行扫描:

public MyTimer() {
    t = new Thread(() -> {
        while (true) {
            try {
                MyTask task = queue.take();
                // 判断是否到了执行时间了
                if (task.delay <= System.currentTimeMillis()) {
                    // 执行任务
                    task.run();
                } else {
                    // 没到时间把任务塞回队列
                    queue.put(task);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

最后就是 schedule 方法了:

public void schedule(Runnable runnable, long delay) {
    // 注册一个任务, 执行时间为: 当前时间+延迟时间
    queue.put(new MyTask(runnable, System.currentTimeMillis() + delay));
}

其实写到这,一个简单的定时器已经写完了,但是这个代码不够好,我们可以做优化!


3. 定时器代码优化

回过头来看我们上述写的 MyTimer 的构造方法,里面是一个 while (true) 循环,也就意味着这个线程要无止境的从队列中读元素,而这个线程会一直占用 CPU 资源。

举个例子,比如我现在定了一个早上 8:00 的闹钟,我 7点醒了,那么我打开手机一看,时间没到,关上手机,立刻又打开手机看,时间没到,关上手机,又马上打开手机看,还是没到时间.....

这里的 while (true) 循环就类似于上面的例子,t 线程从队列中取元素,发现时间没到,塞回队列,第一次循环结束,又从队列中取,发现时间没到,又塞回去....

有必要一直看到没到点吗?能不能让这个线程等一会呢?假设还差 100s,那就让这个线程等 100s 之后再执行嘛!这样还节省了 CPU 资源,更不用反复从队列中 take 和 put 了,也不用重复的向上调整了(堆的特性)!

于是我们就可以使用 wait 带参数版本,让线程主动等一段时间,等当前时间和执行到点时间的时间差就行,那么既然等的时间是明确的,可不可以采用 sleep 呢?

注意!sleep 是不建议的,如果当前需要等 30min 执行任务,那么在 sleep 的过程中,又添加了一个任务呢?只需要 10s 后执行呢?这样可能就会错过新任务的时间!有人说,sleep 不是也是可以唤醒吗?但是 sleep 的唤醒是会抛异常的,这个不推荐!

如果采用 wait 带参数,则会更合适,每次注册任务的时候,都 notify 唤醒一下,重新看堆顶的元素即可。

public MyTimer() {
    t = new Thread(() -> {
        while (true) {
            try {
                MyTask task = queue.take();
                long curTime = System.currentTimeMillis();
                if (curTime <= task.delay) {
                    // 到时间了执行任务
                    task.run();
                } else {
                    // 没到时间先放回队列
                    queue.put(task);
                    // 根据当前时间和任务要执行的时间, 等一个时间差
                    synchronized (this) {
                        this.wait(curTime - task.delay);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

public void schedule(Runnable runnable, long delay) {
    // 注册一个任务, 执行时间为: 当前时间+延迟时间
    queue.put(new MyTask(runnable, System.currentTimeMillis() + delay));
    // 每次有新的任务,都唤醒一下,让线程重新读堆顶元素,防止新任务最先执行
    synchronized (this) {
        this.notify();
    }
}

但是上述代码还存在一个线程安全问题!

当代码执行完 12 行,就被 CPU 切走了,另一个线程开始注册任务,这个任务比堆中的其他任务都先执行,那么此时的 notify 就空喊一嗓子了,当 CPU 切回来时,扫描线程 t 就可能没有感知到又有新的任务注册进来了。

本质上就是 synchronized 范围太小了!我们扩大加锁的范围即可:

public MyTimer() {
    t = new Thread(() -> {
        while (true) {
            try {
                synchronized (this) {
                    MyTask task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime <= task.delay) {
                        // 到时间了执行任务
                        task.run();
                    } else {
                        // 没到时间先放回队列
                        queue.put(task);
                        // 没到根据当前时间和任务要执行的时间, 等一个时间差
                        this.wait(curTime - task.delay);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

这样一来,t 线程读堆顶元素的时候,其他线程就不能放入元素了,等 t 线程执行到 this.wait(),就会自动释放锁,后面其他线程再注册任务的时候,每次 notify 就都是有效的了,t 也能感知到了!保证了每次 notify 都能有效唤醒!

那么实现到这,我们差不多已经能和自带的 Timer 差不多的效果了,但是注意!这里的定时器,不一定那么的准时!而我们目前能写到这个地步也就差不多可以了。


下期预告:【多线程】实现一个线程池

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

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

相关文章

全网IPv6流量监控分析案例

前言 随着某学院IPv6网络的建设和应用投产&#xff0c;在不影响现有应用、网络及用户端的情况下&#xff0c;实时掌握IPv6网络运行状况、用户体验快慢、网络应用性能好坏及网络资源利用等需求已迫在眉睫。 学校率先采用IPv6全流量分析系统 NetInside率先推出支持IPv6的全流量…

高速电路PCB布线还有不会的吗?

数字电路很多时候需要的电流是不连续的&#xff0c;所以对一些高速器件就会产生浪涌电流。如果电源走线很长&#xff0c;则由于浪涌电流的存在进而会导致高频噪声&#xff0c;而此高频噪声会引入到其他信号中去。而在高速电路中必然会存在寄生电感和寄生电阻以及寄生电容&#…

pwn(1)-栈溢出(上)

熟悉栈溢出的原理熟悉栈溢出的防御方法学会栈溢出的利用方法学会栈溢出的奇技淫巧 栈溢出原理和防御&#xff08;一&#xff09; 栈的高地址在下低地址在上&#xff0c;先进入的数据压入栈底。 例如 #include <stdio.h> int add(int a,int b) {return ab; } int main…

一学就会----反转链表

文章目录 题目描述思路一思路二 题目描述 反转一个单链表。 图片示例&#xff1a; 思路一 其实&#xff0c;反转一个单向链表&#xff0c;我们可以看成是将链表中的每个结点的指向反向&#xff08;即从后一个结点指向前一个结点&#xff09;。 我们在考虑情况的时候&#xff0…

PHY6230国产蓝牙BLE5.2 2.4G SoC低成本遥控灯控芯片

PHY6230是高性价比低功耗高性能Bluetooth LE 5.2系统级芯片&#xff0c;集成32-bit高性能低功耗MCU&#xff0c;16KB OTP&#xff0c;8KB Retention SRAM和64KB ROM&#xff0c;可选EEPROM&#xff0c;适用多种PC/手机外设连接、遥控、灯控等场景。 特点&#xff1a; 高性能多…

Python数据攻略-Pandas的数据计算和清洗整理

大家好&#xff0c;我是Mr数据杨&#xff0c;今天带大家以《三国演义》为背景&#xff0c;探索Python数据处理的奥秘。 将眼光投向谋士们&#xff0c;他们就如同Python的算术运算和NumPy、SciPy函数&#xff0c;精准的计算和预测是他们的必备技能。比如&#xff0c;郭嘉分析敌…

使用 GMDH 进行时间序列预测

目录 主要命令 CreateTimeSeriesData FitPolynomial GetPolynomialLayer 分组数据处理方法&#xff08;GMDH&#xff09; PLOT 主要命令 采用分组数据处理方法&#xff08;GMDH&#xff09;对全球冰体积时间序列的建模和预测 fsz size(A) 返回一个行向量&#xff0c;其元…

创建好的提示词来让 Stable Diffusion 生成 AI 艺术作品图像

如何创建好的提示词来让 Stable Diffusion 生成 AI 艺术作品图像&#xff1f; 文章目录 Stable Diffusion如何使用&#xff1f;优秀的提示词如何制作&#xff1f;主题描述 Subject图片类型风格艺术感觉相机、镜头、渲染 示例基础绘图光线和颜色的变化图片类型美术风格艺术风格组…

springboot+java校园二手物品交易系统vxkyj

本项目在开发和设计过程中涉及到原理和技术有: B/S、Java、Jsp、MySQL数据库等等。 系统有以下几点意义&#xff1a; &#xff08;1&#xff09;提供用户和用户之间互利互惠的交易平台。 &#xff08;2&#xff09;操作简单&#xff0c;用户可以在家里就能淘到自己想要的东西&a…

祝贺!Databend 入选 ICT 中国可信云优秀云原生创新案例

2023 年 6 月 6 日&#xff0c;由工业和信息化部主办&#xff0c;中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;、中国邮电器材集团有限公司承办、创原会协办的“ ICT 中国 2023 高层论坛-云原生产业发展论坛”在北京召开。本届论坛以“云智原生新底座&am…

【C++】包装器-bind function

文章目录 包装器function包装器function包装器介绍function包装器统一类型function包装器简化代码的列子function包装器的意义 bind包装器bind包装器介绍bind包装器绑定固定参数bind包装器调整传参顺序bind包装器的意义 包装器 function包装器 function包装器介绍 function包…

【Axure教程】通过输入框动态维护可视化图表

与静态图表相比&#xff0c;动态图表更能吸引观众的眼球并提供更好的视觉效果。动态元素可以吸引观众的注意力&#xff0c;使数据更生动、更具交互性。这有助于提高信息传达的效果&#xff0c;并能够引起观众的兴趣和参与。所以今天作者就教大家&#xff0c;如果通过输入框元件…

[NOIP2003 提高组] 加分二叉树

[NOIP2003 提高组] 加分二叉树 题目描述: 设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n)&#xff0c;其中数字 1,2,3,…,n 为节点编号。每个节点都有一个分数&#xff08;均为正整数&#xff09;&#xff0c;记第 i 个节点的分数为 di​&#xff0c;tree 及它的每个…

优雅草蜻蜓T系统·专业版服务端以及后台部署说明-完整步骤-语音会议室支持多人语音,屏幕分享,导航配置,会议管理,会员管理

蜻蜓T系统专业版服务端以及后台部署 1&#xff0c;解压文件和基础环境配置 将源码用git工具克隆到/www/wwwroot git clone git地址 或者是由优雅草发送的商业源码文件包直接进行解压 ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;…

十分钟柔和护理,轻松舒缓眼疲劳,云康宝眼部按摩仪体验

平时工作、生活中&#xff0c;每天都要长时间对着手机、电脑等电子设备&#xff0c;像是被它们吸走了灵魂&#xff0c;有时候眼睛干干的、痛痛的&#xff0c;像是被沙子刮过&#xff0c;光靠眼药水之类的东西根本解决不了问题&#xff0c;所以趁着618我入手了一款眼部按摩仪&am…

数字系统。网络层。IPv4 子网划分。ICMP

嘿&#xff0c;伙计们&#xff01;我希望你们一切都好。作为我每周更新计算机网络的一部分&#xff0c;我想分享一些令人兴奋的话题。 首先&#xff0c;我们深入研究了数字系统的世界。本主题重点介绍二进制和IPv4地址&#xff0c;我们学习了如何将二进制转换为十进制&#xf…

Zookeeper部署

Zookeeper的安装 环境变量的配置 上传安装包 使用MobaXterm、FinalShell或者使用scp将安装包apache-zookeeper-3.6.3-bin.tar.gz上传到/root/softwares下 复制代码 解压安装 [rootqianfeng01 ~]# tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz -C /usr/local 复制代码 更名 …

1091 Acute Stroke (PAT甲级)

这道题用dfs做的话&#xff0c;因为递归太多层&#xff0c;堆栈溢出&#xff0c;有两个测试点过不了&#xff1b;所以用bfs。 但令我百思不得其解的是&#xff0c;我没用方向变量x[6], y[6], z[6]&#xff0c;直接老老实实算每一个方向的话&#xff0c;最后一个测试点过不了&a…

17.6:迪瑞克斯啦算法

迪瑞克斯啦算法 这个算法研究的是&#xff1a;有向的&#xff0c;没有负权重&#xff0c;可以有环的图。 这个算法主要研究的是&#xff1a;给出的节点到这张图的其他节点的最短路径是多少。用一个表表示出来。 思路&#xff1a; 如下图所示&#xff0c;我们想要求出a节点到其…

建立时间、保持时间和亚稳态

目录 一、建立时间和保持时间 二、亚稳态 三、避免亚稳态策略 四、多级寄存器阻断亚稳态传播 一、建立时间和保持时间 如图1所示&#xff0c;建立时间&#xff08;set up time&#xff09;是指在触发器的时钟信号上升沿到来以前&#xff0c;数据稳定不变的时间&#xff0c;…