Java【多线程基础6】定时器 + 模拟实现Timer

news2024/9/23 13:20:54

文章目录

  • 前言
  • 一、定时器
    • 1, 什么是定时器
    • 2, 如何使用定时器
  • 二、模拟实现定时器
    • 1, 初步实现
    • 2, 问题改善
  • 总结


前言

📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙希望我的专栏能够帮助到你:
JavaSE基础: 从数据类型类和对象, 封装继承多态, 接口, 综合小练习图书管理系统
Java数据结构: 顺序表, 链表, 二叉树, , 哈希表等 (正在持续更新)
JavaEE初阶: 多线程, 网络编程, html, css, js, severlet, http协议, linux等(正在持续更新)

上篇多线程基础5主要介绍了: 阻塞队列的实现原理和使用方式, 并且模拟实现了阻塞队列, 以及讲解了生产者消费者模型的相关内容

本篇继续介绍多线程相关的基础内容, 内容较多, 分为若干篇持续分享


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

一、定时器

1, 什么是定时器

定时器 的主要功能是可以让代码延迟一段时间执行, Java 标准库中封装的类为 Timer , 其核心方法是 schedule , 第一个参数就是程序员指定的要执行的"任务", 第二个参数是指定的延迟时间(单位是毫秒)

并且定时器可以多次调用 shedule 方法, 安排多个任务(毕竟早上起不来是可以多定几个闹钟的)

2, 如何使用定时器

例如, 我想要延迟 5 秒后在控制台输出"已经过了5秒", 延迟 1 秒后在控制台输出"已经过了1秒", 代码如下 :

    public static void main(String[] args) {
        Timer timer = new Timer();
        // 有两个参数: 1, 任务 2, 延迟时间
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("已经过了5秒");
            }
        }, 5000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("已经过了1秒");
            }
        }, 1000);
    }

执行结果 :
在这里插入图片描述

定时器实际使用起来相对简单, 接下来我们模拟实现定时器


二、模拟实现定时器

模拟实现定时器, 首先要搞清楚 Timer 背后都有哪些东西

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
            
            }
        }, 1000);

1️⃣可以看到, schedule 方法的第一个参数是一个 TimerTask 类的对象, 所以我们要实现一个 MyTask 类来表示"任务"

2️⃣上面展示定时器的使用方式的代码, 先描述了延迟 5 秒执行的任务, 后描述了延迟 1 秒执行的任务, 结果控制台先输出了延迟 1 秒的任务, 后输出了延迟 5 秒的任务

根据 Timer 的功能, 以及之前数据结构的学习, 不难推测出 Timer 背后使用了一个类似优先级队列的数据结构来组织管理这些 TimerTask 对象, 哪个任务延迟时间短, 就把这个任务排在前面

就像我睡前定了一个明早上 7:00 的闹钟, 我怕起不来, 又定了一个 6:50 的闹钟, 那么明早肯定是 6:50 的闹钟先响

既然是多线程环境, 那么这个"队列"是需要带有阻塞功能的, 正好 Java 标准库中提供了一个类 PriorityBlockingQueue 表示优先级阻塞队列, 我们直接拿来使用, 队列中的元素类型就是第一点所分析的 TimerTask

Timer 的源码中并没有使用 PriorityBlockingQueue 这个数据结构, 而是封装了一个内部类 TaskQueue , 要更为复杂, 实现出来的功能相同, 为了方便还是使用 PriorityBlockingQueue

3️⃣实际上, Timer 类还有一个内部类 TimerThread , 当作内置的线程, 随着 Timer 类的实例化, 就生成了这个线程, 用来"使用"第二点分析的数据结构(源码中的 TaskQueue 这个内部类), 需要不停判断当前时间是否已经满足需求

如果没有这个功能, 我的程序怎么知道此时此刻是否该执行我安排的 TimerTask 了? 怎么确保我安排的任务准时执行? 所以我们也要实现一个内部类 MyTimerThread 来表示"工作线程", 我们模拟实现的 MyTimerThread 直接对 PriorityBlockingQueue 进行操作


1, 初步实现

根据上述的三点来写出大概的代码框架

1, MyTask 类, 表示"任务", 模拟源码中的 TimerTask 类

👉两个成员属性 : 用来描述任务的对象 和 用来记录执行任务时的绝对时间戳

👉用来描述任务的 command 对象需要是 Runnable 接口类型的, 所以才能重写 run 方法, 并且是用 lambda 表达式的方式
绝对时间戳是 延迟时间 + 当前时间戳

👉MyTask 类需要实现 Comparable 接口并重写 compareTo 方法, 因为接下来 PriorityBlockingQueue 这个优先级阻塞队列的元素之间的比较方式是基于延迟时间比较

public class MyTask implements Comparable<MyTask>{
	// 成员属性
    protected Runnable command; // 1, 用来描述任务的对象
    protected long delay;// 2, 绝对时间戳

    // 构造方法
    public MyTask(Runnable command, long delay) {
        this.command = command;
        this.delay = delay + System.currentTimeMillis();// 绝对时间戳
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.delay - o.delay);
    }
}

2, MyTimer 类, 表示定时器, 模拟源码中的 Timer 类

👉成员属性 : 定义一个 PriorityBlockingQueue 的对象, 元素类型是 MyTask
👉构造方法 : 在实例化 MyTimer 的时候就需要启动"工作线程 MyTaskThread "
👉成员方法 : schedule 只要负责构造出 MyTask 的对象并放入优先级阻塞队列中即可

public class MyTimer {
	// 成员属性 核心数据结构 
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // 构造方法
    public MyTimer() {
        // 需要内置一个线程来组织管理队列中的任务
        Thread myTaskThread = new MyTaskThread();
        myTaskThread .start();
    }

	// 内部类 工作线程
    class MyTaskThread extends Thread {
        @Override
        public void run() {
			// 先省略不写
        }
    }

    // 成员方法: 安排一个任务, 有两个参数: 1, 任务 2, 延迟时间
    public void schedule(Runnable command, long delay) {
        // 1, 先构造出来 task 对象
        MyTask task = new MyTask(command, delay);

        // 2, 往队列中添加任务
        queue.put(task);
    }
}

3, 内部类 MyTaskThread 表示工作线程 , 模拟源码中的 TimerThread 类

👉既然 MyTaskThread 表示一个线程, 就需要继承 Thread 类, 重写 run 方法

👉MyTaskThread 这个线程的主要作用是 : 判断优先级阻塞队列中队首的任务是否需要执行
需要把队首的任务从队列中取出, 判断当前时间戳 curTime 和 这个任务的绝对时间戳 delay 的大小关系, 如果 curTime < delay 说明还没到点儿, 再把这个任务放回去就好了, 否则说明正好到点或者已经晚了, 需要立即执行

上述过程需要在一个 while(true) 中死循环, "时时刻刻"判断是否已经到点儿了

	class MyTaskThread  extends Thread {
        @Override
        public void run() {
            // 判断是否该执行此任务
            while(true) {
                try {
                    // 取出队首元素
                    MyTask task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if(curTime < task.delay) {
                        // 时间还没到, 再放回队列中
                        queue.put(task);
                    }else {
                        // 时间到了, 执行任务
                        task.command.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

2, 问题改善

上述代码中的第三点, MyTaskThread 中重写的 run 方法还存在一个问题 : 忙等

要知道 CPU 的执行指令的速度是很快的, 每秒可以执行上亿条指令, 假设现在的时间是晚上 22:00, 我要安排一个明早 8:00 执行的任务. 在这 10 个小时中, CPU 在不停的执行 MyTaskThread 这个线程, 执行的操作无非就是取出队首元素再放回去, 反复执行的次数是一个天文数字, 这就造成了忙等, 是对 CPU 资源的浪费

✅改善这一问题的方式就是 : 使用 wait 方法
还假设现在的时间是晚上 22:00, 任务执行的时间是明早 8:00 , 现在工作线程执行了一遍循环体, 此时如果让线程阻塞等待 10 个小时, 然后再继续执行循环, 就可以直接执行任务, 这就避免了忙等, 避免了 CPU 资源的浪费

⚠️注意 :
wait 方法给定的参数是 task.dely - curTime, 并且 wait 方法需要搭配notify 方法, 都要搭配锁使用

notify 方法要写在 schedule 方法中, 每新增一个任务之后就 notify 唤醒正在阻塞等待的工作线程, 这样也不用担心线程在阻塞等待时, 加入了新的任务而无法及时执行
既然要搭配锁使用, 那么 MyTimer 类就需要再多定义一个成员属性 : 锁对象 lock

在这里插入图片描述

完整的 MyTimer 类的代码 :

public class MyTimer {
    // 成员属性: 1, 核心数据结构, 优先级阻塞队列 2, 锁对象
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    private final Object lock = new Object();

    // 构造方法
    public MyTimer() {
        // 需要内置一个线程来扫描管理队列中的任务
        Thread myTaskThread = new MyTaskThread ();
        myTaskThread .start();
    }
	
	// 内部类, 工作线程
    class MyTaskThread extends Thread {
        @Override
        public void run() {
            // 判断是否该执行此任务
            while(true) {
                try {
                    synchronized (lock) {
                        // 取出队首元素
                        MyTask task = queue.take();
                        long curTime = System.currentTimeMillis();
                        if(curTime < task.delay) {
                            // 时间还没到,再放回队列中,并且阻塞等待
                            queue.put(task);
                            lock.wait(task.delay - curTime);
                        }else {
                            // 时间到了,执行任务
                            task.command.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 成员方法:安排一个任务,有两个参数: 1,任务 2,延迟时间
    public void schedule(Runnable command, long delay) {
        // 1,先构造出来 task
        MyTask task = new MyTask(command, delay);

        // 2,往队列中添加任务
        queue.put(task);

        // 3,使用notify
        synchronized (lock) {
            lock.notify();
        }
    }
}

MyTask 类的代码没有修改, 还是上面写的那样


总结

以上就是本篇的全部内容, 主要介绍了定时器的使用方式和实现原理, 以及模拟实现 Timer 类
模拟实现的过程主要有三点需要多加思考 :
1️⃣MyTask 类, 表示"任务", 模拟源码中的 TimerTask 类
2️⃣MyTimer 类, 表示定时器, 模拟源码中的 Timer 类, 其中用 PriorityBlockingQueue 代替了源码中的 TaskQueue 类
3️⃣内部类 myTaskThread 表示工作线程 , 模拟源码中的 TimerThread 类

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

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

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

相关文章

【设计模式】责任链模式的介绍及其应用

责任链的介绍 责任链模式是一种对象的行为模式。在责任链模式里&#xff0c;很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递&#xff0c;直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求&a…

一款非常经典的蓝牙多媒体芯片​RDA5850

RDA5850是一款高度集成的蓝牙无线电收发器和基带处理器&#xff0c;基于低成本的32位XCPU RISC核心&#xff08;其实虽然手册上说是RISC核心&#xff0c;但我怎么感觉这款芯片有点MIPS的影子 &#xff09;具有多媒体能力。此芯片有着丰富的外设&#xff0c;而大多数的功能都是…

找出3个数中的最大值与最小值

设计完整的程序实现以下功能&#xff1a;从键盘上输入3个整数&#xff0c;通过指针运算&#xff0c;找出3个数中的最大值与最小值和它们的地址&#xff0c;并实现最大值与最小值的交换&#xff08;要求用指针作为函数参数处理&#xff09;。 #include <stdio.h>void swap…

onnxruntim的使用方法

onnxruntime是谁发明的&#xff1f; ONNX Runtime 是由微软公司开发和维护的深度学习推理框架。ONNX Runtime 的前身是 Microsoft Cognitive Toolkit (CNTK)&#xff0c;它是微软公司开发的一个深度学习框架&#xff0c;支持多种硬件平台和操作系统&#xff0c;具有高性能和易…

杜甫经典长诗“三吏”“三别”赏析

杜甫简介 杜甫&#xff08;公元712—公元770&#xff09;&#xff0c;原籍湖北襄阳&#xff0c;后徙河南巩县。字子美&#xff0c;自号少陵野老&#xff0c;杜少陵&#xff0c;杜工部等&#xff0c;唐代著名诗人&#xff0c;世称“诗圣”&#xff0c;生活在安史之乱(唐朝由盛转…

时间管理:瞎忙和高效的区别

前言 最近看到一个非常有价值的文章&#xff0c;内容上确实震撼到我了&#xff0c;借鉴过来用于自勉。 人和人的差距为什么这么大&#xff1f; 而且这种差距&#xff0c;并不是家庭背景、权利财富或天赋带来的&#xff0c;仅仅是我们对时间的掌控&#xff0c;人总是错把忙碌…

B860AV2.1-A/M/B_1g/2g-通刷_当贝纯净桌面-线刷固件包

ZTE_B860AV2.1-A&#xff0f;M&#xff0f;B_1g&#xff0f;2g-通刷_当贝纯净桌面-线刷固件包-内有教程及短接点 特点&#xff1a; 1、适用于对应型号的电视盒子刷机&#xff1b; 2、开放原厂固件屏蔽的市场安装和u盘安装apk&#xff1b; 3、修改dns&#xff0c;三网通用&a…

交直流一体化电源系统-交流电源自动测试系统ATECLOUD-Power

现代社会离不开电力&#xff0c;而电力供应的质量和可靠性&#xff0c;对于各个领域的发展都有着至关重要的作用。交直流一体化电源系统作为一种新型的供电方案&#xff0c;不仅具备高度的适应性、可靠稳定性以及节能环保性等诸多优势&#xff0c;还能为各行各业的市场提供更加…

【构造】CF634div3 D. Anti-Sudoku

Problem - D - Codeforces 题意&#xff1a; 给定一个数独&#xff0c;让你更改最多9格&#xff0c;使其变成anti-数独&#xff0c;即每一行&#xff0c;每一列&#xff0c;每一个3*3大格都存在一个数的出现次数>2 思路&#xff1a; 构造题&#xff0c;要不是根据题目隐含…

初识Vue-组件

目录 组件注册 全局注册 局部注册 全局导入 按需载入 Vue组件的生命周期 动态组件 keep-alive【使用的是LRU淘汰算法】 组件注册 全局注册 全局注册的组件可以在任何地方使用 Vue.component("custom-a", {render() {return <div>custom-a</div>…

代码随想录算法训练营day30 | 332. 重新安排行程,51. N 皇后,37. 解数独

代码随想录算法训练营day30 | 332. 重新安排行程&#xff0c;51. N 皇后&#xff0c;37. 解数独&#xff08;难度大&#xff0c;先简单了解&#xff09; 332. 重新安排行程51. N 皇后解法一&#xff1a;回溯 37. 解数独解法一&#xff1a; 总结 332. 重新安排行程 没有视频题解…

乐鑫esp32-c2开发板 烧录演示

一、准备工作 数据线X 1 、 四博智联 ESPC2-12 开发板 X 1 二、环境搭建 1、进入https://code.visualstudio.com 官网下载VSCODE软件 2、安装完成后安装乐鑫插件如下图 3、插件安装完后&#xff0c;查看- 命令面板&#xff08;快捷键CtrlShiftP&#xff09;。 4、输入config…

SpringSecurity 认证流程源码详细解读

一、SpringSecurity 本质探寻 SpringSecurity 的原理其实就是一个过滤器链&#xff0c;内部包含了提供各种功能的过滤器。这样说肯定非常枯燥&#xff0c;所以接下来还是在代码中看一看。 前期工作&#xff0c;需要在代码中引入 SpringSecurity 依赖&#xff0c;这里不再赘述…

服务(第十七篇)mysql的高级语句

mysql 6大常见的约束&#xff1a; 主键约束&#xff1a;primay key 主键字段不允许有重复的记录&#xff0c;不允许为null&#xff0c;一个表只能有一个主键 唯一性约束&#xff1a;unique key 唯一键字段不允许有重复的记录&#xff0c;但允许为null&#xff0c;一…

unordered系列容器的底层——哈希

目录 unordered系列容器的底层结构 哈希概念 哈希冲突 哈希函数 常见哈希函数 哈希冲突解决 闭散列 线性探测 哈希表什么情况下进行扩容&#xff1f;如何扩容&#xff1f; 二次探测 开散列 开散列概念 开散列增容 存储string类型的解决 闭散列的实现 开散列的实…

IS210AEBIH3BEC隔离器用于变压器等高压设备

IS210AEBIH3BEC隔离器用于变压器等高压设备 隔离器可以根据在电力系统中的位置进行分类 母线侧隔离器——隔离器直接连接到主母线线路侧隔离器 - 隔离器将放置在任何馈线的线路侧Transfer bus side isolator – isolator will be directly connected with the transfer bus S…

python自动化爬虫实战

python自动化爬虫实战 偶然的一次机会再次用到爬虫&#xff0c;借此机会记录一下爬虫的学习经历&#xff0c;方便后续复用。 需求&#xff1a;爬取网站数据并存入的csv文件中&#xff0c;总体分为两步 爬取网站数据存到到csv文件中 1、配置爬虫环境 1.1、下载自动化测试驱动 …

基于transformer的Seq2Seq机器翻译模型训练、预测教程

前言 机器翻译&#xff08;Machine Translation, MT&#xff09;是一类将某种语言&#xff08;源语言&#xff0c;source language&#xff09;的句子 x x x翻译成另一种语言&#xff08;目标语言&#xff0c;target language&#xff09;的句子 y y y 的任务。机器翻译的相关…

深度学习 - 46.DIN 深度兴趣网络

目录 一.引言 二.摘要 ABSTRACT 三.介绍 INTRODUCTION 1.CTR 在广告系统的作用 2.传统 MLP 存在的问题 3.DIN 的改进 四.近期工作 RELATEDWORK 1.传统推荐算法 2.用户行为抽取 五.背景 BACKGROUD 六.深度兴趣网络 DEEP INTEREST NETWORK 1.特征表示 Feature Repres…

ChatGPT的强化学习部分介绍——PPO算法实战LunarLander-v2

PPO算法 近线策略优化算法&#xff08;Proximal Policy Optimization Algorithms&#xff09; 即属于AC框架下的算法&#xff0c;在采样策略梯度算法训练方法的同时&#xff0c;重复利用历史采样的数据进行网络参数更新&#xff0c;提升了策略梯度方法的学习效率。 PPO重要的突…