文章目录
- 前言
- 一、定时器
- 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 类
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~
上山总比下山辛苦
下篇文章见