当前位置: 首页 > news >正文

多线程初阶(十):定时器 模拟实现

目录

1. 定时器

1.1 概念

1.2 Timer

1.2.1 schedule

 1.2.2 TimerTask

2. 模拟实现定时器

2.1 MyTimerTask

2.2 MyTimer

2.2.1 构造方法

2.2.2 schedule

2.3 模拟实现定时器 --- 整体代码


1. 定时器

1.1 概念

定时器, 简单来说, 就像我们生活中的 "闹钟", 我们提前设置好时间, 等时间一到, 就会执行相关操作.

1.2 Timer

Java 标准库中也提供了定时器 --- Timer.

Timer 中是通过一个线程, 来扫描队首元素, 并执行任务.

1.2.1 schedule

Timer 中提供了 schedule 方法, 我们通过 schedule 来传入想要执行的任务 task, 以及任务执行的时间 delay (从当前时刻开始计算, 推迟 delay(ms) 后执行任务).

 1.2.2 TimerTask

TimerTask 是一个抽象类, 实现了 Runnable 接口.

所以在定时器 Timer 中, 通过 TimerTask 来描述要执行的任务.

(核心还是重写 run 方法)

所以, 通过 schedule 将任务和执行时间放入定时器中, 其中使用 TimerTask 来描述任务(重写 run).

使用匿名内部类形式描述任务: 

  1. 创建继承自 TimerTask 的子类(匿名)
  2. 重写 run
  3. new 子类对象


2. 模拟实现定时器

实现定时器, 有以下关键几步:

  1. 创建一个描述任务的类
  2. 使用一个集合类, 管理任务(优先级队列, 保证取出的任务是最早要执行的) => 优先级队列(堆)
  3. 实现 schedule 方法, 把要执行的任务放到队列中
  4. 创建一个线程, 执行队列中的任务

2.1 MyTimerTask

这里创建 MyTimerTask 作为任务相关的类.

在类中定义两个属性:

  1. 任务(这里将任务 Runnable 抽象为类的属性, 也可以通过类实现 Runnable 接口)
  2. 时间段(从任务入队后时的时间戳开始计时, 经过该时间段后, 执行任务)

在构造方法中对这两个属性完成初始化.

因为一个 MyTimerTask 对象, 就是一个任务, 也是队列中的一个元素, 所以元素入队时, 要按照执行的先后顺序进行排序, 确保队首元素是最早要执行的, 所以要对时间段这一属性规定比较规则.

排序规则的指定有以下两种:

  1. 实现 Comparable, 重写 compareTo
  2. 实现 Comparator, 重写 compare(这里采取这一种) 

此外, 还有实现 run 方法, 保证从队列中取出元素后, 进行执行操作.

/*** 任务*/
class MyTimerTask implements Comparable<MyTimerTask> {private long time;private Runnable task;public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}public void run() {task.run();}@Overridepublic int compareTo(MyTimerTask o) {// 最早执行的任务 排在队首return (int) (this.getTime() - o.getTime());}public long getTime() {return time;}
}

2.2 MyTimer

MyTimer 就做为我们模拟实现的 定时器.

在 MyTimer 中, 我们要实现 schedule 方法, 并且要创建一个线程来执行队列中的任务.

定时器中的线程和调用 schedule 的线程(将任务放到队列中)同时对队列进行操作, 必然涉及到线程安全问题, 所以要使用 synchronized 对相关操作进行加锁.

需要一个优先级队列, 对任务进行存储(可以将最早执行的任务放在队首).

MyTimer 主要完成构造方法和 schedule 的实现.

2.2.1 构造方法

当队列中有元素时, 队首元素即为最早要执行的任务, 我们要对任务的执行时机进行判断, 不能早也不能晚.

所以线程应时刻就绪, 一旦队列不为空, 就要做好执行任务的准备.

所以, 在构造方法中, 要进行线程的创建, 定时器创建好后, 线程也创建好了.

  1. 当队列有任务存在时, 等到任务指定的时间一到, 线程就执行该任务.
  2. 当队列为空时, 线程进行 wait 阻塞等待, 等 schedule 安排新任务后, 再进行 notify 唤醒.

 注意, 当队列为空时, 线程一定要 阻塞等待, 避免不工作却一直消耗 cpu 资源.

并且, wait 的等待可能存在被 Interrupt 提前唤醒的风险, 所以要所以 while 进行二次判断.

不仅当队列为空时, 线程要进行等待, 当队列不为空,  但队首元素的执行时间未到时, 也要进行等待.(任务要执行时的时间戳 > 当前时刻的时间戳 => 时间未到)

  1. 这里的等待要使用 wait 带有超时时间的版本, 参数为: 未来执行时的时间戳 - 当前时间戳, 时间一到时, 立即执行任务
  2. 而不能使用 sleep 进行等待, 因为 sleep 休眠期间是不会释放锁的(抱着锁睡), 意味着 schedule 将会因为没有获取到锁也陷入阻塞等待状态, 导致不能使新任务入队.

在这里有一个细节点, 队空时的 wait 和 任务执行时间未到时的 wait , 都可能被 schedule 的 notify 唤醒:

  1. 队空时的 wait 被 notify 唤醒, 执行后续操作, 没问题.
  2. 但是等待任务执行时间的 wait 被唤醒后(schedule 添加新元素, 执行 notify), 因为最外层的 while , 所以线程会再次取队首元素, 重新进行 wait . 所以这样以来, 不仅没问题, 并且会重新对队首元素任务执行时间和当前时刻进行判定, 防止新 schedule 任务的执行时间更提前

时间戳:

1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数.

通俗来说, 平时几时几分几秒的时间, 是给人看的 "时刻".

而 时间戳, 是给计算机看的 "时刻".

获取系统时间戳的 api :

 System.currentTimeMillis() => 返回的是一个 long类型, ms级别的时间戳

public MyTimer() {Thread t = new Thread(() -> {try {// 调用 schedule 的线程和定时器内部的线程, 同时操作一个队列, 必然存在线程安全问题// 构造方法本身可以写 synchronized ,但是这里不能这么写// 要保护的逻辑是线程中 run 内部的逻辑synchronized (this) {// lambda 也能捕获到外部类的 this, lambda 可以捕获到 final 或 事实final , this就是事实 finalwhile (true) {// 加上 while 再次确实, 防止被 Interrupt 提前唤醒(如果被提前唤醒的, 那队列空就操作队列就出问题了)while (queue.isEmpty()) {// 队列为空就 wait, 防止一直占用 cpu 资源, 防止 "忙等"this.wait();}MyTimerTask peek = queue.peek();// 判断时间到没到if (peek.getTime() - System.currentTimeMillis() > 0) {// 没到就 wait// 当有新任务入队, 这里的 wait 也会被唤醒, 重新取队首任务(重新取最早执行的任务)进行时间的判断// 若没有新任务入队, 则 wait 一直休眠直至超时时间到来, 执行任务// 不能所以 sleep, sleep是抱着锁睡, 会导致不能 schedulethis.wait(peek.getTime() - System.currentTimeMillis());} else {// 执行任务MyTimerTask task = queue.poll();task.run();}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}

2.2.2 schedule

将任务放到队列中, 再进行 notify 操作.

这里的 notify 有两个作用:

  1. 结束队列为空时, 线程的阻塞等待.
  2. 新任务可能比原先队首的任务执行时间要更早, 所以唤醒正在等待旧任务执行时间到来的线程, 让线程重新获取队首元素, 重新进行时间判定.
public void schedule(Runnable task, long time) {synchronized (this) {MyTimerTask myTimerTask = new MyTimerTask(task, time + System.currentTimeMillis());queue.offer(myTimerTask);// 唤醒队空时和 时间未到 时的 waitthis.notify();}}

2.3 模拟实现定时器 --- 整体代码

/*** 任务*/
class MyTimerTask implements Comparable<MyTimerTask> {private long time;private Runnable task;public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}public void run() {task.run();}@Overridepublic int compareTo(MyTimerTask o) {// 最早执行的任务 排在队首return (int) (this.getTime() - o.getTime());}public long getTime() {return time;}
}/*** 定时器*/
class MyTimer {PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();public MyTimer() {Thread t = new Thread(() -> {try {// 调用 schedule 的线程和定时器内部的线程, 同时操作一个队列, 必然存在线程安全问题// 构造方法本身可以写 synchronized ,但是这里不能这么写// 要保护的逻辑是线程中 run 内部的逻辑synchronized (this) {// lambda 也能捕获到外部类的 this, lambda 可以捕获到 final 或 事实final , this就是事实 finalwhile (true) {// 加上 while 再次确实, 防止被 Interrupt 提前唤醒(如果被提前唤醒的, 那队列空就操作队列就出问题了)while (queue.isEmpty()) {// 队列为空就 wait, 防止一直占用 cpu 资源, 防止 "忙等"this.wait();}MyTimerTask peek = queue.peek();// 判断时间到没到if (peek.getTime() - System.currentTimeMillis() > 0) {// 没到就 wait// 当有新任务入队, 这里的 wait 也会被唤醒, 重新取队首任务(重新取最早执行的任务)进行时间的判断// 若没有新任务入队, 则 wait 一直休眠直至超时时间到来, 执行任务// 不能所以 sleep, sleep是抱着锁睡, 会导致不能 schedulethis.wait(peek.getTime() - System.currentTimeMillis());} else {// 执行任务MyTimerTask task = queue.poll();task.run();}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable task, long time) {synchronized (this) {MyTimerTask myTimerTask = new MyTimerTask(task, time + System.currentTimeMillis());queue.offer(myTimerTask);// 唤醒队空时和 时间未到 时的 waitthis.notify();}}
}public class Demo31 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(() -> {System.out.println("hello 3000");}, 3000);myTimer.schedule(() -> {System.out.println("hello 2000");}, 2000);myTimer.schedule(() -> {System.out.println("hello 1000");}, 1000);}
}

END


http://www.mrgr.cn/news/56608.html

相关文章:

  • 展会亮点回顾|HMS汽车工业通信解决方案
  • C/C++中const用法
  • RHCE的学习(4)
  • 树莓派使用Node.js 将蓝牙设置成BLE外设
  • 平面声波——一维Helmhotz波动方程
  • 携程线下一面,面试内容:
  • Docker安装ocserv教程(效果极佳)
  • Golang | Leetcode Golang题解之第502题IPO
  • RIGOL示波器 AUTO键功能已被限制,怎么解决?
  • 大规模图形计算框架之HAMA
  • Apache配置案例一:完成web服务的一个基本应用
  • 读数据工程之道:设计和构建健壮的数据系统17存储的原材料
  • 导出你的大脑:AI如何成为个人认知的延伸
  • MATLAB人脸考勤系统
  • stm32 单片机(on-chip flash)(片上flash)使用 rt-thread 的FAL 软件包
  • Python | Leetcode Python题解之第502题IPO
  • 利用 Direct3D 绘制几何体—7.编译着色器
  • OracleSQL语句 某字段重复数据只取一条
  • word中某些段落行间距无法更改
  • Java 之 Map遍历并删除的几种方法对比
  • 一种用于传感器网络的新型OPC UA PubSub协议绑定(MQTT-SN)
  • go 语言 Gin Web 框架的实现原理探究
  • Java | Leetcode Java题解之第501题二叉搜索树中的众数
  • 有什么好点子帮助更好的学习英语吗?
  • MySQL-事物隔离级别
  • C++ —— 实现一个日期类