1. java标准库的计时器
1.1 关于计时器
计时器类似闹钟,有定时的功能,其主要是到时间就会执行某一操作,即可以指定时间,去执行某一逻辑(某一代码)。
1.2 计时器的简单介绍
在java标准库中,提供了Timer类,Timer类的核心方法是schedule(里面包含两个参数,一个是要执行的任务代码,一个是设置多久之后执行这个任务代码的时间)
注意:Timer内置了线程(前台线程),代码如下所示:
package thread;import java.util.Timer;
import java.util.TimerTask;public class ThreadDemo30 {public static void main(String[] args) throws InterruptedException {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {// 时间到了之后, 要执行的代码System.out.println("hello timer 3000");}}, 3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello timer 2000");}}, 2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello timer 1000");}}, 1000);System.out.println("hello main");Thread.sleep(5000);timer.cancel();}
}
结果如下图所示:
代码分析:
如上图所示,先打印 hello main ,等过了1s才打印 hello 1000,说明Timer内置了线程,main线程不用等待,而timer类是要到时间才会执行任务代码。
为什么这里可以看到idea里显示线程结束,因为timer类里面有cancel方法,可以结束线程,我们把cancel方法加到打印hello 3000那方法里面,这样就可以结束timer类里面的线程了。
注意:timer类里面内置的是前台线程,会阻止线程提前结束。
2. 模拟实现一个计时器
2.1 设计思路
1、计数器中要存放任务的数据结构
首先,计时器可以定时去执行一些任务操作,那么我们怎么每次先去执行时靠前的那一操作呢?其实在某一些场景下确实可以用数组,但这就需要我们每次都去遍历数组,找出最靠前的时间,但是如果我们要定时很多任务,都需要先找到时间靠前的任务,这就不合理了;从数组里面找出这个时间最靠前的任务数据,一方面要考虑资源花销大的问题,还有要考虑时间的问题,找任务的时间太长,错过了已经到时要执行的任务,如上所述说明使用数组存放任务是不合理的。
所以就引入了优先级队列,这样每次拿都能拿到时间最小的任务,时间复杂度也仅仅是O(1),但是优先级队列不能是阻塞队列,否则会引起死锁问题。
2、存放优先级队列中的任务类型:
我们自定义为任务类MyTimerTask
任务类是放要执行的代码和要执行任务时间,单独作为一类,存进优先级队列中,其中,优先级队列里的比较规则是按任务类设定的执行时间先后(即时间的大小)来比较的。3、计数器类MyTimer
我们设计一个线程,放在MyTimer类的构造方法中,这个线程就是扫描线程,我们使用该扫描线程来完成判断和操作,主要是入队列或者判断啥时候才执行要执行的代码的操作;同时创建任务schedule的方法里面也包含有入队列的操作。
2.2 代码实现
1、MyTimer类:
// 通过这个类, 来表示一个定时器 class MyTimer {// 负责扫描任务队列, 执行任务的线程.private Thread t = null;// 任务队列private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 搞个锁对象, 此处使用 this 也可以.private Object locker = new Object();public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);// 添加新的元素之后, 就可以唤醒扫描线程的 wait 了.locker.notify();}}public void cancel() {// 结束 t 线程即可// interrupt}// 构造方法. 创建扫描线程, 让扫描线程来完成判定和执行.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 {// 当前时间还没到, 暂时先不执行// 不能使用 sleep. 会错过新的任务, 也无法释放锁.// Thread.sleep(task.getTime() - curTime);locker.wait(task.getTime() - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});// 要记得 start !!!!t.start();} }
里面的核心模块:
1、schedule方法,该方法的创建任务,里面包含了要执行的代码和执行代码的时间, 2、构造方法,里面有一个线程,该线程就是不断去判断队列有没有任务,如果有任务的话,就去找最先执行的任务,等到该任务执行时间就执行扫描到的该任务,如果没到达执行时间的话就要等。
2、MyTimerTask任务类:
// 通过这个类, 来描述一个任务 class MyTimerTask implements Comparable<MyTimerTask> {// 在什么时间点来执行这个任务.// 此处约定这个 time 是一个 ms 级别的时间戳.private long time;// 实际任务要执行的代码.private Runnable runnable;public long getTime() {return time;}// delay 期望是一个 "相对时间"public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;// 计算一下真正要执行任务的绝对时间. (使用绝对时间, 方便判定任务是否到达时间的)this.time = System.currentTimeMillis() + delay;}public void run() {runnable.run();}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time - o.time);// return (int) (o.time - this.time);} }
该任务类里面放的是要执行的任务,和任务执行的延迟时间时间,因为任务要放进优先级队列里,所以要构造一个比较器,用时间参数来进行比较,并且重写compareTo方法,将比较规则具体化。
2.3 计时器的线程安全
1、维护队列进出的操作---加锁
不创建其他线程,如果只有一个主线程去调用MyTimer类的话,此时就会有主线程main和 t 线程,这时候,存在线程不安全问题的主线程的代码如下所示:
public class TimerTest {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 1000");}}, 1000);System.out.println("hello main");}
}
关于主线程main与t线程存在的线程安全图解如下:
多线程运行时,会出现同一时刻一个队列存在多个任务进有出的情况,会导致线程不安全;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;
对于入队列操作上锁的位置范围,就是把创建任务和入队列操作都上锁;
对于出队列操作上锁的位置范围,我们要考虑是否把while循环都给上锁了,显然易见,把while上锁的代码十分危险,在我们当前的场景上确实可以用;但是,在其他场景下,如果一个线程拿到锁了,系统就会不停的解锁、加锁,这样会导致其他线程饿死了,所以在while里面加锁,是比较大众的;
2、优先级队列为空时,设置阻塞等待功能
3、任务没到执行时间,要让该任务等待到固定时间在执行
代码完善部分如下所示:
代码详解:
没到任务执行的时间,就要让该任务阻塞等待,且等待时间是: 任务执行的时刻 - 当前的时刻,没有限制要等待的时间的话,就会一直循环,每次循都会环判断是不是到任务执行的时间了,反复循环这个代码执行速度是很快的,但是就会盲等,由此我们不设置任务执行时间的话就会导致计算机资源的浪费;
ps:本次关于计时器的内容就到这里了,如果对大家有所帮助的话,就请一键三连,当然内容可能还会更新,因为未完待续嘛!!!