定时器相当于是一个闹“闹钟”
在代码中,也经常需要“闹钟”机制
网络通信中,经常需要设定一个“超时时间”
方法 | 作用 |
void schedule(TimerTask task, long delay) | 指定delay时间之后(单位毫秒)执行任务task |
基本效果
使用Java标准库中的Timer,在 3s
后打印“hello
”:
import java.util.Timer; import java.util.TimerTask; public class Demo4 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello"); } },3000); System.out.println("程序开始执行"); } }
还可以制定多个任务:
import java.util.Timer; import java.util.TimerTask; public class Demo4 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello1"); } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello2"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello3"); } },3000); System.out.println("程序开始执行"); } }
定时器的实现
对于定时器来说
- 创建类,描述一个要执行的任务是什么
- 内容
- 时间
- 创建多个任务,通过一定的数据结构,把多个任务存起来
- 有专门的线程,执行这里任务
创建一个任务
schedule
的时候,指定的时间是“delay
”值,但是,描述任务的时候,不建议使用delay
来表示,最好使用“绝对时间”(时间戳)来表示- 那之后就很容易判断当前任务是否要执行,可以随时对比当前的时间戳和记录的时间戳,看是否达到了
- 如果当前时间戳
>
记录的时间戳,那时间就到了,若<
,时间就还没到 - 如果你写了一个
delay
的话,就不好对比了
- 这样就没有了参照物,没有了比较对象
- 比如说你写了一个
delay=5000
,但你不知道什么时候是这个时间(刻舟求剑)
//定时器的任务 class MyTimerTask { //描述任务是什么 private Runnable runnable; //通过毫秒时间戳,表示这个任务具体啥时候执行 private long time; public MyTimerTask(Runnable runnable, long delay) { this.runnable = runnable; //获取当前时间戳 + delay,得到一个绝对的时间戳 this.time = System.currentTimeMillis() + delay; } public void run() { runnable.run(); } public long getTime() { return time; } }
- 若此时为
12:00
,设置的delay
为1 h
,那么此时的time
就为13:00
创建多个任务
数据结构的选择
此时我们最先需要做的就是确定是用什么数据结构来存放多任务
- List
- 不是一个好的选择,比较低效
- 后续执行列表中的任务的时候,就需要依次遍历每个元素。执行完毕还需要把对应的任务从
List
中删掉
- 堆
- 可以高效方便地找到“最小/第二小/第三小”的值,而我们的定时器就是按照时间顺序来执行任务的。
- 这样我们只要确定好时间最小的任务,判定它是否到时间需要执行了即可。若时间最小的任务都还没到时间,那其他任务就更还没到了
因为使用的是优先级队列,所以要指定出比较规则,才能排出优先级
- 在
MyTimeTask
类中实现Comparable
接口
class MyTimerTask implements Comparable<MyTimerTask>{ ... ... ... @Override public int compareTo(MyTimerTask o) { return (int)(this.time - o.time); } } class MyTimer { private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); }
- 上面
comparaTo
里作差的顺序就决定了是大堆还是小堆
- 这里我们需要的是小堆
- 这里是谁减谁,不要背,可以先写成一个顺序,试试就知道了
多线程的执行
此时,各元素可以被顺利地添加到这个优先级队列中了,各个任务已经可以被我们用优先级队列管理起来了
之后我们就需要考虑得有人去执行这里面的任务
- 就是说,现在我们已经有了队列,得有专门的线程去这个队列里面取元素,然后去执行里面的任务
- 并且执行之前,在取元素的时候还需要判定时间,看时间是不是符合我们的要求
class MyTimer { private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); public MyTimer() { //创建线程,负责执行上述队列中的内容 Thread t = new Thread(() -> { //我们也不知里面有多少个元素,就需要不停地循环去取 while(true){ //在队列不为空的情况下,取出对首元素 if(queue.isEmpty()){ continue; } MyTimerTask current = queue.peek(); if(System.currentTimeMillis() >= current.getTime()){ //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑 current.run(); //把执行过的任务,从队列中删除 queue.poll(); }else { //不执行任务 continue; } } }); t.start(); } }
System.currentTimeMillis() >= current.getTime()
- 比如,当前时间是
10:30
,任务时间是12:00
,不应该执行
- 直接跳过这次循环
- 如果当前的时间是
10:30
,任务时间是10:29
,就应该执行
- 先执行
runnable
中的run
方法,随后使用poll
将这个元素从队列中删去
- 在这个循环中,首先取到的是时间最靠前的任务(因为是小堆排序),再取就是第二靠前的任务
之后我们就需要给定时器里面安排任务,实现 schedule
方法
class MyTimer { ... ... ... public void schedule(Runnable runnable, long delay) { MyTimerTask myTimerTask = new MyTimerTask(runnable,delay); queue.offer(myTimerTask); } }
- 实例化一个
MyTimeTask
对象,参数为所要执行的具体任务和时间,随后将其添加到队列中
特别注意
class MyTimer { private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); public MyTimer() { //创建线程,负责执行上述队列中的内容 Thread t = new Thread(() -> { //我们也不知里面有多少个元素,就需要不停地循环去取 while(true){ //在队列不为空的情况下,取出对首元素 if(queue.isEmpty()){ continue; } MyTimerTask current = queue.peek(); if(System.currentTimeMillis() >= current.getTime()){ //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑 current.run(); //把执行过的任务,从队列中删除 queue.poll(); }else { //不执行任务 continue; } } }); t.start(); } }
对于此处的 while
循环
- 若初始情况下,队列中没有任何元素
- 此处的逻辑就会在短时间内进行大量的循环,并且这些循环都是没什么意义的,就类似于“线程饿死”
- 所以,我们就将这里的
continue
操作改为wait/notify
操作。在空的时候wait
,在不空的时候notify
(schedule
之后) - 这样,如果队列是空的,就会进行
wait
,避免无意义的循环,直到进行schedule
操作之后,将其notify
while
里面的判空
- 将
if
改为while
更加安全 - 避免
wait
被一些其他的方式唤醒了,结果队列还是为空,往下走进行peek
操作,就会出现问题 - 改为
while
后,即使被意外唤醒了,也能够继续确认,是不是要继续wait
- 假设队列中,已经包含一些元素,当前时间是
10:45
,任务时间是12:00
- 这样,线程也会一直循环执行,检查时间到没到:没到就
continue
,然后继续进行循环判定 - 类似于:我定了一个
12:00
的闹钟,此时我一看是10:45
,我继续睡一会,刚一闭眼,又睁眼看时间,为10:45
,我又闭眼;刚一闭眼,我又睁眼看时间… - 这种情况下,并没有完成什么实质性的工作,但还要一直进行循环,也类似于“线程饿死”
- 所以,此时我们也将这里的
continue
改为wait
,但此时就不需要用notify
进行唤醒了,我们可以指定一个“超时时间”(当前时间距离任务时间还有多久) - 注意:此时不应该使用
sleep
,因为可能存在这样的情况:
- 你在
sleep
的过程中,新来了一个时间更早的任务,但线程无法被提前唤醒;若使用wait
的话,每次在schedule
的时候都会进行notify
将线程唤醒,让线程再次进行判断,重新设置等待时间,这样就不会错过新的任务 sleep
在休眠的时候是不会释放锁的,这样就会造成进行取出和删除操作的线程是抱着锁睡的,之后schedule
就拿不到锁了,就进行不了新增任务的操作了
我们可以将两个wait
的异常try-catch
一起放在外面
改后:
class MyTimer { private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); private static Object locker = new Object(); public MyTimer() { //创建线程,负责执行上述队列中的内容 Thread t = new Thread(() -> { try { //我们也不知里面有多少个元素,就需要不停地循环去取 while (true) { synchronized (locker) { //在队列不为空的情况下,取出对首元素 while (queue.isEmpty()) { locker.wait(); } MyTimerTask current = queue.peek(); if (System.currentTimeMillis() >= current.getTime()) { //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑 current.run(); //把执行过的任务,从队列中删除 queue.poll(); } else { //不执行任务 locker.wait(current.getTime() - System.currentTimeMillis()); } } } } catch (InterruptedException e) { throw new RuntimeException(e); } }); t.start(); } public void schedule(Runnable runnable, long delay) { MyTimerTask myTimerTask = new MyTimerTask(runnable, delay); synchronized (locker) { queue.offer(myTimerTask); locker.notify(); } } }
线程安全问题
在定时器里面,我们在这里涉及到的核心是 Queue
这个数据结构,
在这个 Queue
这里,我们有一个专门的线程,从队列里面取元素,删除元素
然后我们还有一个 schedule
方法,去执行插入队列操作
- 此时我们发现,
schedule
里面的插入操作是一个线程,取和删操作是另一个线程 - 那么此时存在多个线程同时修改
Queue
,那就肯定是存在线程安全风险的 - 尤其是我们用的还是
PriorityQueue
,这个类自身不带线程安全的控制能力
所以,此时就一定存在线程安全的风险
为了解决这个线程安全问题,我们就给操作都加上锁
class MyTimer { private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); private static Object locker = new Object(); public MyTimer() { //创建线程,负责执行上述队列中的内容 Thread t = new Thread(() -> { //我们也不知里面有多少个元素,就需要不停地循环去取 while (true) { synchronized (locker) { //在队列不为空的情况下,取出对首元素 if (queue.isEmpty()) continue; MyTimerTask current = queue.peek(); if (System.currentTimeMillis() >= current.getTime()) { //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑 current.run(); //把执行过的任务,从队列中删除 queue.poll(); } else { //不执行任务 continue; } } } }); t.start(); } public void schedule(Runnable runnable, long delay) { MyTimerTask myTimerTask = new MyTimerTask(runnable, delay); synchronized (locker) { queue.offer(myTimerTask); } } }
执行效果
主程序:
public class Demo { 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); System.out.println("程序开始执行"); } } //执行结构 程序开始执行 hello 1000 hello 2000 hello 3000