Java【多线程基础6】定时器的使用方式 + 模拟实现Timer

news2024/10/5 18:23:44

文章目录

  • 前言
  • 一、定时器
    • 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/518379.html

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

相关文章

6.文本三剑客--sed、awk

文章目录 文本三剑客sed介绍命令介绍打印内容删除替换插入分组 文本三剑客 sed 介绍 sed编辑器 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来 编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些命令要么…

高阶python | 堆栈列表:RPN应用(模拟逆波兰式功能实现)

python版本&#xff1a;3.10 在列表中&#xff0c;append和pop方法有一个特殊的用途。可以在列表上使用这两个方法让列表变成一个堆栈使用。 这就是一个栈&#xff0c;它是先进后出&#xff0c;类似单门轿厢电梯一样的设计&#xff0c;出入口共用 堆栈最有用的应用之一就是做逆…

【社区团购】预制菜零售如何打造精准社群?

预制菜作为现代生活的新型“网红”食品&#xff0c;其受欢迎程度日益提高。而在商业竞争日益激烈的当下&#xff0c;如何让你的预制菜零售业务&#xff08;文章编辑ycy6221&#xff09;具有巨大的竞争优势呢&#xff1f;社区团购是一个不错的切入点&#xff0c;这不仅是为了扩大…

5月12号软件资讯更新合集.....

Vue 3.3 “浪客剑心” 发布 Vue 3.3 已正式发布&#xff0c;代号 "Rurouni Kenshin"&#xff08;浪客剑心&#xff09;。 公告写道&#xff0c;此版本专注于改进开发者使用体验 —— 特别是 SFC<script setup> 与 TypeScript 的结合使用。一同发布的还有 Vue…

【C语言】操作符详解(上)

操作符详解&#xff08;上&#xff09; 1.操作符分类2.算数操作符3.移位操作符3.1 右移3.2 左移 4.位操作符4.1位操作符发的应用 5.赋值操作符6.单目操作符7.关系操作符8.逻辑操作符 1.操作符分类 算术操作符移位操作符位操作符赋值操作符单目操作符关系操作符逻辑操作符条件操…

Vue3-黑马(四)

目录&#xff1a; &#xff08;1&#xff09;vue3-基础-axios-获取数据 &#xff08;2&#xff09;vue3-基础-axios-发送数据 &#xff08;3&#xff09;vue3-基础-axios-baseURL &#xff08;1&#xff09;vue3-基础-axios-获取数据 第三方库axios是对xhr的封装&#xff0…

智安网络|网络安全威胁风险分析:识别以及预防黑客和钓鱼攻击

随着网络技术的不断发展和普及&#xff0c;网络安全问题日益严峻。黑客、病毒、恶意软件、钓鱼攻击等威胁不断涌现&#xff0c;给个人、企业、国家的信息安全带来了极大的威胁。如何识别、分析和预防网络安全威胁已成为所有人必须要解决的问题。本篇文章将从黑客攻击和钓鱼攻击…

proc文件系统

proc介绍 (1)proc是虚拟文件系统&#xff0c;虚拟的意思就是proc文件系统里的文件不对应硬盘上任何文件&#xff0c;我们用去查看proc目录下的文件大小都是零,是接受到请求才动态生成的&#xff1b; (2)proc文件系统是开放给上层了解内核运行状态的窗口&#xff0c;通过读取pro…

如何通过品牌矩阵号赋能品牌?

小红书作为年轻人的“消费决策”平台、逐步成为越来越多用户的消费指南&#xff0c;同时也变成众多品牌的营销基地。在小红书运营矩阵账号可以很好的树立品牌形象、增加粉丝粘性、节约广告成本&#xff0c;那么在搭建矩阵的过程中如何管理品牌矩阵号也成为众多品牌必须要思考的…

3.操作系统

文章目录 1.操作系统概述&#xff08;1&#xff09;当前操作系统的主要类型&#xff08;2&#xff09;操作系统的特点&#xff08;3&#xff09;五种主要的功能&#xff08;4&#xff09;考点分布 2.进程&#xff08;1&#xff09;进程的状态&#xff08;2&#xff09;进程的定…

jeecg低代码主从表打印

1.在在线开发菜单栏中选择online表单开发&#xff0c;新建两张表&#xff0c;分别为主表和附表。 2.首页进入报表设计——积木报表设计。 3.进入报表管理——打印设计&#xff0c;选择一个模板或者新建都可以。 4.进入设计页面&#xff0c;点击数据集管理右边的添加数据表&a…

高通开发系列 - 驱动模块Oops后如何找到出错位置

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 问题概述问题现象出错定位问题概述 加载驱动模块时发生:Kernel panic - not syncing: Fatal exception 那么如何找到驱动模块出错的…

抖音AI绘画变成真人软件

抖音AI绘画成人软件的发展可能包括以下几个方面&#xff1a; 算法优化&#xff1a;随着深度学习算法的不断发展&#xff0c;未来该技术可能会通过改进模型架构、优化训练方式等手段&#xff0c;提高生成图像的质量和自然度。 多样化的绘画风格&#xff1a;为了满足用户…

【软考备战·希赛网每日一练】2023年5月12日

文章目录 一、今日成绩二、错题总结第一题第二题第三题第四题 三、知识查缺 题目及解析来源&#xff1a;2023年05月12日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 一般情况下&#xff0c;一旦Web服务器向浏览器发送了请求数据&#xff0c;它就要…

2021 第十二届蓝桥杯大赛软件赛决赛, 国赛,C/C++ 大学B组题解

2021 第十二届蓝桥杯大赛软件赛决赛, 国赛&#xff0c;C/C 大学B组题解 文章目录 第1题 —— 带宽 &#xff08;5分&#xff09;第2题 —— 纯质数 &#xff08;5分&#xff09;第3题 —— 完全日期 &#xff08;10分&#xff09;第4题 —— 最小权值 &#xff08;10分&#xf…

MySQL之快速查询的秘籍 【B+树索引】

前言 本文章收录在MySQL性能优化原理实战专栏&#xff0c;点击此处查看更多优质内容。 本文摘录自 ▪ 小孩子4919《MySQL是怎样运行的&#xff1a;从根儿上理解MySQL》 &#x1f604;学完前面我们讲解了InnoDB数据页的7个组成部分&#xff0c;知道了各个数据页可以组成一个双…

Linux 多线程(3)多线程应用、信号量、线程池、单例模式

应用 生产者与消费者模型 生产者与消费者模型---设计模式的一种 设计模式--程序员针对一些典型的应用场景所给出的一种典型的解决方案 应用场景--在有大量数据任务产生的同时需要进行任务处理的场景 如果采用单执行流解决&#xff1a; 效率低下 资源利用不一定合理&#xff08;…

【Python数据分析】常用内置函数(一)

&#x1f64b;‍ 哈喽大家好&#xff0c;本次是python数据分析、挖掘与可视化专栏第四期 ⭐本期内容&#xff1a;常用内置函数 &#x1f3c6;系列专栏&#xff1a;Python数据分析、挖掘与可视化 &#x1f44d;“总有一段时光悄悄过去然后永远怀念.” 文章目录 前言类型转换int(…

Vue---列表渲染

目录 一、列表渲染的基本使用 &#xff08;1&#xff09;渲染数组数据 &#xff08;2&#xff09;渲染对象数据 二、v-for指令的高级用法 三、列表过滤 四、列表排序 一、列表渲染的基本使用 使用v-for指令可以将数组&#xff0c;对象数据渲染成列表视图 &#xff08;1&a…

前端自学好还是培训好?女生有多适合学前端,我来告诉你!

2023年了&#xff0c;你是否还在迷茫或者每个月拿着5/6k做着卷死的工作&#xff0c;不但存不下钱还不能好好享受生活&#xff0c;如果是&#xff0c;那你真该考虑一下转行了。 好程序员先说说前端到底怎么开始学&#xff1a; 有的伙伴说今年28岁了&#xff0c;学的会计&#xf…