定时器,是我们日常开发所常用的组件工具,类似于闹钟,设定一个时间,当时间到了之后,定时器可以自动的去执行某个逻辑
目录
MyTimer 构造方法,创建扫描线程,让扫描线程来完成判定和执行
第二个问题:如果一直执行 while 那就一直在加锁和解锁...
Timer 的基本使用
Java 标准库中,也提供了定时器的实现。
创建一个 timer 对象之后,调用 timer 的schedule 方法,在schedule 方法中,参数是一个匿名内部类,重写 run 方法,run 方法的方法体中,就是我们时间到了之后要执行的代码,还有一个参数是我们要等待的时间(单位是ms)(这里不可以使用 lambda 表达式。lambda 表达式得是函数时接口才行,即 interface 里面只能有一个方法)
定义一个 timer 添加多个任务,多个任务同时会带有一个时间:
打印结果如下:
且当打印完成之后,进程并不会结束,Timer 里内置了一个线程(前台线程)
timer 并不知道我们的代码是否还会添加新的任务进来,处在“严阵以待”的状态,我们需要使用 cancel 来主动结束,否则 Timer 是不知道是否其他地方还要继续添加任务的。
实现一个 Timer
Timer 里面要包含那些内容呢?
1. 需要一个线程,负责帮我们来掐时间。等任务到达合适的时间,这个线程就负责执行。
2. 还需要一个队列 / 数组,能够保存所有 schedule 进来的任务。
直观想,如果这个线程,不停的去扫描上述队列中的每个元素,看每个任务的时间否达到,到时间就执行。(但如果这个队列很长,这个遍历的过程的开销就很大了 O(N) )
==》 优先级队列!!!
每个任务都是带有 delay 时间的,一定是先执行时间小的,后执行时间大的。就不需要对上述队列进行遍历了,只需要关注队首元素是否到时间。(如果队首没到时间,后续其他元素也就一定没到时间) ==》 可以使用标准库提供的PriorityQueu(线程不安全),也有 PriorityBlockingQueue(线程安全)(但在我们此处的场景中,PriorityBlockingQueue 不太好控制,容易出问题)(我们可以对 PriorityQueue进行手动加锁,确保线程安全)
开始实现:
通过这个类,来描述一个任务
时间戳:以 1970 年 1 月 1 日 0 时 0 分 0 秒,为基准,计算当前时刻和基准时刻的 秒数 / 毫秒数 / 微秒数...
delay 是一个“相对”的时间间隔,也就是以当前时刻为基准,计算 3000 ms 之后才执行。
针对上面的类,还有什么比较重要的问题呢???==》 我们上面的类,是要放在优先级队列中的。有些集合类,是对元素有特定要求的(PriorityQueu TreeMap TreeSet 都是要求元素是“可比较大小的” ==》 Comparable,Comparator)(HashMap,HashSet 则是要求元素是“可比较相等” “可 hash 的”==》 equals hashCode)
实现 Comparable 接口,重写 compareTo 方法
此处是期望根据时间,时间小的作为优先级更高...(当我们要使用 compareTo 的时候,千万不要背到底是那个 - 那个,直接写代码试一试即可!!! ==》 作为程序员,要“扬长避短”,记忆这个事情,并不是我们人脑擅长的...)
// 通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
// 在什么时间点来执行这个任务
// 在此约定这个 time 是一个 ms 级别的时间戳
private long time;
// 实际任务要执行的代码:
private Runnable runnable;
//delay 是一个 “相对时间”
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//计算一下真正要执行任务的绝对时间
//(使用绝对时间,方便判定任务是否到达时间)
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
public long getTime() {
return time;
}
}
通过这个类,来表示一个定时器
其中定义了 t 线程,负责扫描任务队列,执行任务。
用优先级队列创建一个 queue,其中元素存放 MyTimer(我们刚刚创建的任务类)
在 schedule 方法中,传入两个参数 runnable 和 delay,将 task 添加入 queue 当添加成功的时候, notify 锁
MyTimer 构造方法,创建扫描线程,让扫描线程来完成判定和执行
当前这个代码,还至少有两个核心问题,需要解决。
问题一: 线程安全问题
在主线程中,我们 new 新的 Timer 然后调用 schedule 方法来添加任务。
在 MyTimer 中,我们 new 了个优先级队列 queue 作为成员变量,但优先级队列是线程不安全的,需要进行加锁来解决线程安全问题。
我们是在另一个线程中,即 MyTimer 的构造方法中的 t 线程中,对队列进行删除,应该在这个线程中加锁。
在这个线程中,synchronized 加在哪里呢?我们先大致观察,发现 t 线程中,都是写操作,为了要实现我们前面提到的原子性,就可以给整个线程加锁 ==》
但这样加锁,是正确的吗???
我们在主线程中 new 一个 MyTimer 对象
然后进入 MyTimer 的构造方法:
构造方法就是 t 线程,这个线程进来之后,就直接加锁了,加锁之后,才会进入 while 循环,while 循环结束了之后,锁才能释放,但有没有一种可能,我们的 while 循环结束不了,导致我们的锁无法释放...
主线程下面的语句 myTimer 调用 schedule 方法
schedule 方法中也在尝试加锁,但锁被构造方法中占用着,这里就没办法加上锁了...
一番调整之后,我们可以把 synchronized 加在 while 里面,这样才会有释放锁的机会,外面才有可能拿到锁
第二个问题:如果一直执行 while 那就一直在加锁和解锁...
我们上面的代码,当队列为空,没有任务的时候,直接 continue,然后又 while,同样的,时间还没到,直接 continue,然后又 while,这里两个代码继续循环是没有意义的,应该等一等!!!
这里两个代码的执行速度是非常快的,当解锁之后,马上又会进入 while 循环,立即又重新尝试加锁了,导致其他线程想要通过 schedule 加锁,但是加不上(线程饿死)
==》 解决方法:引入 wait
注意: 在当时间还没到,暂时不执行中,使用 sleep 是不太合适的。(可能我们会想,当 curTime >= task.getTime() 不满足的时候,那我们 sleep task.getTime() - curTIme 时间即可,但这样是不行的!)
但我们可以写为 locker.wait(task.getTime() - curTime)
1. wait 的过程中,有新的任务来了,wait 就会被唤醒!!!schedule 有 notify的!会根据新的任务重新计算要等待的时间
2. wait 的过程中,没有新的任务,时间到了,按照原定计划,执行之前的这个最早的任务即可。
但 wait 我们在前面讲过,wait 可能存在“虚假唤醒”的情况,即在 wait 的等待过程中,发现了一些意料之外的情况,所以前面我们是用 while 来进行多次确定的,这里的话,我们可以再进行一次任务检查。 ==》
这样,我们就成功实现了一个 Timer
完整代码如下:
import java.util.PriorityQueue;
// 通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
// 在什么时间点来执行这个任务
// 在此约定这个 time 是一个 ms 级别的时间戳
private long time;
// 实际任务要执行的代码:
private Runnable runnable;
//delay 是一个 “相对时间”
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//计算一下真正要执行任务的绝对时间
//(使用绝对时间,方便判定任务是否到达时间)
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
public long getTime() {
return time;
}
}
// 通过这个类,来表示一个定时器
class MyTimer {
// 负责扫描任务队列,执行任务的线程
private Thread t = null;
// 任务队列
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 构造方法,用来创建扫描线程,让扫描线程来完成判定和执行
public MyTimer() {
t = new Thread(() -> {
// 扫描线程就需要循环的 反复的扫描队首元素,然后判定队首元素是不是时间到了
// 如果时间没到,什么都不做
// 如果时间到了,就执行这个任务 并且把这个任务从队列中删除
while (true) {
try {
synchronized (locker) {
while (queue.isEmpty()) {
//如果队列为空,即没有任务
// 先不处理
locker.wait();
}
MyTimerTask task = queue.peek();
// 读取到当前时间
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 当前时间已经到了任务时间,就可以执行任务了
queue.poll(); // 先把任务从队列中出来
task.run(); // 执行任务
} else {
// 当前时间还没到,暂时先不执行
locker.wait(task.getTime() - curTime);
if (curTime >= task.getTime()) {
queue.poll();
task.run();
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public class ThreadDemo41 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
}, 1000);
}
}
流程图:
执行顺序解析:
注意:
这里的 wait 之后,还需要再跟一个条件判断。
当我们加了再次条件检查之后,就算是被唤醒了之后,但因为有我们的条件检查,(当前时间还未达到任务执行时间),此时代码中虽然没有显示地再次调用 wait 方法让线程继续等待,但由于整个扫描线程的逻辑是在一个无限循环 while(true) 中,在这一轮循环结束后,会进入下一轮循环,在下一轮循环中,会再次检查任务队列的状态
当再次检查的时候,发现 curTine 仍然小于 task.getTime(),就会再次执行 locker.wait(task.getTime() - curTime)让线程继续等待,直到时间到达,或者再次被唤醒并满足条件。所以,虽然在唤醒后没有立即再次调用 wait 方法,但通过循环结构和条件判断,最终还是实现了线程在条件不满足的时候继续等待的逻辑。
还有就算,在我们的代码中,schedule 方法等待的时间,是相对于调用 schedule 方法时的当前时间而言的,在我们的示例代码中,第一个 schedule 方法调用之后,1000ms 之后会输出 “hello 1000”,第二个 schedule 方法调用之后,2000ms 之后会输出“hello 2000”,第三个 schedule 方法调用之后,3000ms 之后会输出“hello 3000”.
但是我们这里的三个 schedule 方法调用的时间几乎是同步,所以差不多就是在执行程序之后的 3s 之后会输出“hello 3000”