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() {
@Override
public void run() {
// 时间到了之后, 要执行的代码
System.out.println("hello timer 3000");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer 2000");
}
}, 2000);
timer.schedule(new TimerTask() {
@Override
public 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(); } @Override public 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() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
}, 1000);
System.out.println("hello main");
}
}
关于主线程main与t线程存在的线程安全图解如下:
多线程运行时,会出现同一时刻一个队列存在多个任务进有出的情况,会导致线程不安全;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;
对于入队列操作上锁的位置范围,就是把创建任务和入队列操作都上锁;
对于出队列操作上锁的位置范围,我们要考虑是否把while循环都给上锁了,显然易见,把while上锁的代码十分危险,在我们当前的场景上确实可以用;但是,在其他场景下,如果一个线程拿到锁了,系统就会不停的解锁、加锁,这样会导致其他线程饿死了,所以在while里面加锁,是比较大众的;
2、优先级队列为空时,设置阻塞等待功能
3、任务没到执行时间,要让该任务等待到固定时间在执行
代码完善部分如下所示:
代码详解:
没到任务执行的时间,就要让该任务阻塞等待,且等待时间是: 任务执行的时刻 - 当前的时刻,没有限制要等待的时间的话,就会一直循环,每次循都会环判断是不是到任务执行的时间了,反复循环这个代码执行速度是很快的,但是就会盲等,由此我们不设置任务执行时间的话就会导致计算机资源的浪费;
ps:本次关于计时器的内容就到这里了,如果对大家有所帮助的话,就请一键三连,当然内容可能还会更新,因为未完待续嘛!!!