1.0定时器
概念
- 定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定 好的代码.
- 前端后端都有使用
标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为
schedule
. schedule
包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).- 请认准java.util中的time
- 给timer 中注册的一个任务,不是在调用
schedule
的线程中执行的,而是通过Timer内部的线程,来负责执行的,实现一到,timer内部的线程就会调用run - Timer内部,有自己的线程.为了保证随时可以处理新安排的任务,这个线程会持续执行并且这个线程还是前台线程,其他线程都结束了他都不会结束
- 一个定时器中可能有很多任务
语法
//定时器
public class Demo21 {
public static void main(String[] args) {
Timer timer = new Timer();
//利用了lambda匿名内部类,TimerTask实现了runnable(重写),但相比runnable会继续一些额外的时间属性
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器运行,我是定时器任务2");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器运行,我是定时器任务1");
}
}, 2000);
System.out.println("程序开始运行~~");
}
}
2.0 自己实现一个定时器
定时器的构成
思想:
- 先要把一个任务给描述出来,在使用数据结构把多个任务组织起来
数据结构:
- 一个带优先级的阻塞队列 (因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来)
- 创建一个TimerTask 这样的类,表示一个任务,这个任务就需要包含两个方面,任务的内容,任务的实际执行时间(使用时间戳表示,在schedule的时候,先获取到当前的系统时间,在这个基础上加上delay时间间隔),这一步相当于描述
- 队列中的每个元素是一个 Task 对象.
- Task 中带有一个时间属性, 队首元素就是即将执行的对象
- 同时有一个 worker 线程扫描队首元素, 看队首元素是否需要执行,获取一次队首元素事件后,和当前的系统时间,做一个差值,根据这个差值来决定休眠/等待(休眠和等待不会消耗cpu的资源)的时间,这个时间到达之前,不会进行重复扫描
Task类
- Task 类用于描述一个任务(作为 Timer 的内部类). 里面包含一个 Runnable 对象和一个 time(毫秒时 间戳)
- 这个对象需要放到 优先队列 中. 需要进行比较,因此需要实现
Comparable
接口.
代码实现:
// 创建一个类, 用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
// 任务啥时候执行. 毫秒级的时间戳(是相对于系统时间加上多少毫秒).
private long time;
// 任务具体是啥.
private Runnable runnable;
//runnable一般都是一个子类重写了runnable,这个子类在传参的时候一般都是向上转型,所以用父类引用来接收传参(多态),减少类型判断的压力
public MyTimerTask(Runnable runnable, long delay) {
// delay 是一个相对的时间差. 形如 3000 这样的数值.
// 构造 time 要根据当前系统时间和 delay 进行构造.
time = System.currentTimeMillis() + delay;//System.currentTimeMillis()获取系统时间戳
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {
// 认为时间小的, 优先级高. 最终时间最小的元素, 就会放到队首.
// 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
// 随便写一个顺序, 然后实验一下就行了.
return (int) (this.time - o.time);
// return (int) (o.time - this.time);
}
}
Timer类
- Timer 类提供的核心接口为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行.
- Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象. 通过 schedule 来往队列中插入一个个 Task 对象
- Timer 类中存在一个 worker 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.
- 引入一个 obj locker 对象, 借助该对象的 wait / notify 来解决 while (true)不断扫描优先级队列首元素而造成的的忙等问题(占用cpu资源).
代码实现:
// 定时器类的本体,用来组织
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 t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
while (queue.isEmpty()) {
// 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
// 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
// 也会陷入一个高频占用 cpu 的状态(忙等).
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) {
e.printStackTrace();
}
}
});
t.start();
}
}
线程安全的问题
问题1:
PriorityQueue<MyTimerTask> queue
这个集合不是线程安全的- 既会在当前主线程中被使用(主线程调用schedule这个方法时会涉及到队列的插入),又会在扫描线程中使用主线程判断队列(扫描队列会对队列扫描判空和看时间变化)
解决1:
- 在每次针对queue的操作中,进行加锁即可
- 即在schedule方法中和扫描队列中都加一个相同对象的锁
问题2:
- 扫描线程中,我们直接使用sleep进行休眠是不合适的:
- 1、sleep进入阻塞后不会释放锁,会导致锁依旧被扫描线程所持有,导致其他线程无法访问schedule方法,
- 2、sleep在休眠的过程中,不方便提前中断(虽然可以使用interrupt 来中断, 应为interrupt 意味着线程要结束了,结束前干点什么,但这里并不想让线程结束,单纯想让sleep被唤醒
解决2:
- 运用方法,使得能把之前的休眠状态给唤醒,并且根据当前的最新的任务情况,重新进行判定,此时的扫描线程据可以按照队列最新更新最快执行的时间来安排阻塞了
- 可以用wait来代替sleep,就不会像sleep一样被唤醒就代表着中断进程,而是继续执行
问题3:
- 不能让随便一个类,他的对象都能放到优先队列当中
解决3:
- 要求放到优先级队列中的元素时刻比较的,继承Comparable或者写个Comparator类
整体代码
/**
* Created with IntelliJ IDEA.
* Description:
* User: Ap0stoL2
* Date: 2023-07-30
* Time: 11:44
*/
import java.util.PriorityQueue;
// 创建一个类, 用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
// 任务啥时候执行. 毫秒级的时间戳(是相对于系统时间加上多少毫秒).
private long time;
// 任务具体是啥.
private Runnable runnable;
//runnable一般都是一个子类重写了runnable,这个子类在传参的时候一般都是向上转型,所以用父类引用来接收传参(多态),减少类型判断的压力
public MyTimerTask(Runnable runnable, long delay) {
// delay 是一个相对的时间差. 形如 3000 这样的数值.
// 构造 time 要根据当前系统时间和 delay 进行构造.
time = System.currentTimeMillis() + delay;//System.currentTimeMillis()获取系统时间戳
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {
// 认为时间小的, 优先级高. 最终时间最小的元素, 就会放到队首.
// 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
// 随便写一个顺序, 然后实验一下就行了.
return (int) (this.time - o.time);
// return (int) (o.time - this.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 t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
while (queue.isEmpty()) {
// 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
// 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
// 也会陷入一个高频占用 cpu 的状态(忙等).
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) {
e.printStackTrace();
}
}
});
t.start();
}
}
// 写一个定时器
public class Demo22 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3");
}
}, 3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1");
}
}, 1000);
System.out.println("程序开始运行");
}
}