文章目录
- 前言
- 1. 什么是定时器
- 2. 标准库中的定时器
- 3. 自己实现一个定时器
- 总结
前言
本文主要给大家讲解多线程的一个重要案例 — 定时器.
关注收藏, 开始学习吧🧐
1. 什么是定时器
定时器也是软件开发中的一个重要组件 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
- 定时器是一种实际开发中非常常用的组件, 前端后端开发都会使用到.
- 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
- 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.
2. 标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
我们来使用一下标准库中的定时器.
import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo20 {
public static void main(String[] args) {
Timer timer = new Timer();
// 给 timer 中注册的这个任务, 不是在调用 schedule 的线程中执行的.
// 而是通过 Timer 内部的线程, 来负责执行的.
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 3");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2");
}
}, 2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1");
}
}, 1000);
System.out.println("program start");
}
}
代码效果
可以看到, hello 1 2 3 按照时间设置依次执行. Timer 内部, 有着自己的线程. 为了保证随时可以处理新安排的任务, 这个线程会持续执行, 并且这个线程是前台线程, 不可以被打断.
接下来我们自己尝试来实现一个定时器
3. 自己实现一个定时器
要想实现一个定时器, 我们需要先想一想定时器其中的主要逻辑. 我们需要用一个类 (TimerTask) 将一个任务给描述出来, 再用一个类 (Timer) 使用数据结构将多个任务给组织起来.
- 创建一个 TimerTask 这样的类, 用来表示一个任务. 这个任务需要包含两个方面, 任务的内容, 任务需要执行的实际时间. 我们可以使用时间戳来表示, 在 schedule 中, 先获取到当前的系统时间, 在这个基础上, 加上 delay 时间间隔, 就得到了该任务需要执行的实际时间了.
- 创建一个 Timer 这样的类, 使用合适的数据结构, 把多个任务给组织起来. 在这里我们使用优先级队列 PriorityQueue 来实现是最合适的了, 这样队首元素永远都是第一个需要被执行的任务. 我们还需要一个扫描线程, 在获取到队首元素时间后, 根据差值来决定等待时间, 在这个时间到达之前, 不进行重复扫描, 降低扫描次数.
在实现定时器时, 我们有了以上的逻辑支撑, 还需要注意三个关键问题:
- 要保证线程安全, 我们需要给针对 queue 的操作进行加锁.
- 在扫描线程中, 不使用 sleep 来进行休眠, 而是用锁操作 wait 来代替. 因为 sleep 进入阻塞后, 不会释放锁, 这样会影响到其余进程, 如 schedule 的添加任务操作. 并且 sleep 在休眠过程中, 不方便提前中断.
- 我们放到优先级队列中的元素, 必须是 “可比较的”, 需要通过 Comparable 或者 Compartor 定义任务之间的比较规则. 按照需要被执行的时间顺序来比较.
掌握以上几点后, 我们便可以开始动手实现一个定时器了.
import java.util.PriorityQueue;
class MyTimerTask implements Comparable<MyTimerTask> {
private Runnable runnable;
private long time;
public MyTimerTask (Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
// return (int)(o.time - this.time);
return (int)(this.time - o.time);
}
}
class MyTimer {
// 使用优先级队列, 来保存上述的 N 个任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
// 用来加锁的对象
private Object locker = new Object();
// 定时器的核心方法, 就是要把执行的任务添加到队列中.
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
// 每次来新的任务, 都唤醒一下之前的扫描线程, 让扫描线程根据新的任务重新规划时间.
locker.notify();
}
}
// MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
// 就要调用这里的 Runnable 的 Run 方法来完成任务
public MyTimer() {
// 扫描线程
Thread thread = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
while (queue.isEmpty()) {
// 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
// 此处使用 wait 等待更合适.
// 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
// 也会陷入一个高频占用 cpu 的状态(忙等).
// continue;
locker.wait();
}
// 拿出目前需要最早执行的任务
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 假设当前时间是 14:01, 任务时间是 14:00,
// 此时就意味着应该要执行这个任务了.
queue.poll();
task.getRunnable().run();
} else {
// 让当前扫描线程休眠一下, 按照时间差来进行休眠.
// Thread.sleep(task.getTime() - curTime);
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
总结
✨ 本文讲解了多线程案例中的一个经典案例, 定时器, 带大家简单了解了一下标准库库中的定时器, 重点需要掌握其核心的逻辑是什么, 如何自己去实现一个定时器.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!