目录
一、标准库中的计时器
1、计时器的概念
2、计时器的简单介绍
二、模拟实现一个计时器
1、思路
(1)计数器中要存放任务的数据结构
(2)存放优先级队列中的类型:自定义任务类MyTimerTask
(3)计数器类MyTimer
MyTimer类:
MyTimerTask任务类:
2、分析计时器的线程安全问题
(1)维护队列进出的操作
(2)当队列是空的,就要阻塞等待
(3)如果没到时间,就要等待到时在执行要执行的代码
一、标准库中的计时器
1、计时器的概念
计时器类似闹钟,有定时的功能,闹钟是到时间就会响,而计时器是到时间就会执行某一操作,可以指定时间,去执行某一任务(某一代码)。
2、计时器的简单介绍
在标准库中,提供了Timer类,Timer类的核心方法是schedule,里面包含两个参数,一个是要执行的任务代码,一个是设置多久之后执行这个任务代码的时间。注意:Timer内置了线程(前台线程)
代码演示:
public class Test1 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 3000"); } }, 3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 2000"); } }, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 1000"); } }, 1000); System.out.println("hello main"); } }
执行结果:
可以看到先打印 hello main ,等过了1s才打印 hello 1000,往后继续推,说明Timer内置了线程,main线程不用等待,而timer类是要到时间才会执行任务代码。注意:这里的线程并没有结束,可以看到idea里也没有显示线程结束,说明timer类里面内置的是前台线程。
但是timer类里面有cancel方法,可以结束线程,我们把这个方法加到打印hello 3000那方法里面,这样就可以结束timer类里面的线程了。
代码:
public class Test1 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 3000"); timer.cancel(); } }, 3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 2000"); } }, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello 1000"); } }, 1000); System.out.println("hello main"); } }
执行结果:
可以结束线程。
二、模拟实现一个计时器
1、思路
(1)计数器中要存放任务的数据结构
首先,我们知道,计时器是可以定时去执行一些任务操作,那么我们怎么每次先去执行时间小的那一操作呢?用数组吗?其实在某一些场景下确实可以用数组,但这就需要我们每次都去遍历数组,找出最小的时间,但是如果我们要定时很多任务,成千上万呢?这就不合理了,从数组里面找出这个时间最小的数据,一方面要考虑资源花销大的问题,还有要考虑时间的问题,找的时间太长,错过了已经到时要执行的任务,这说明,使用数组存放任务是不合理的。
可以用优先级队列,这样,每次拿都能拿到时间最小的任务,时间复杂度也仅仅是O(1),但是优先级队列不能是阻塞队列,不然会引起死锁问题。
(2)存放优先级队列中的类型:自定义任务类MyTimerTask
任务类是放要执行的代码和要执行任务时间,单独作为一类,存进优先级队列中,其中,优先级队列里的比较是按任务类的时间大小来比较的。
(3)计数器类MyTimer
里面有一个线程,放在MyTimer类的构造方法中,这个线程就是扫描线程,而这个扫描线程来完成判断和操作,入队列或者判断啥时候才执行要执行的代码的操作;还有创建任务schedule的方法,里面也有入队列的操作。
代码:
MyTimer类:
//通过这个类表示定时器
class MyTimer {
Object locker = new Object();
//负责扫描任务队列,执行任务的线程
private Thread t = null;
//任务队列
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//入队列的方法
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
locker.notify();
}
}
//构造方法,会创建线程,让扫描线程来完成判定和执行
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);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
里面的核心代码:schedule方法,这是创建任务,里面包含了要执行的代码和执行代码的时间,还有就是构造方法,里面有一个线程,这个线程就是不断去判断队列有没有任务,到时间了的任务就拿队伍里时间最小的任务,执行这任务里的代码,没到时间就要等。
MyTimerTask任务类:
代码:
//通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
//在什么时间点来执行任务
private long time;//此处的time是一个ms级别的时间戳
//实际任务要执行的代码
private Runnable runnable;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//计算的是要执行任务绝对时间,方便判断是否到达时间了
this.time = System.currentTimeMillis() + delay;
}
//得到要执行任务时间的方法
public long getTime() {
return this.time;
}
public void run() {
this.runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
}
任务类里面放要是和执行的代码,和要执行代码的时间,因为要放进队列里,所以要实现一个比较器,用时间来比较,重写compareTo方法。
2、分析计时器的线程安全问题
(1)维护队列进出的操作
我们知道,不创建其他线程,就一个主线程去调用MyTimer类的话,一共就会有两个线程:主线程和 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");
}
}
主线程有入队列的操作,但 t 线程也有出队列的操作,如图:
多线程操作一个队列有进有出,肯定是线程不安全的操作;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;
入队列操作上锁位置好知道,把创建任务和入队列操作都上锁;但是出队列呢?要在哪里上锁,把while循环都给上锁了?显然,这样的代码感觉有点危险,在这场景上确实可以用,但是,如果是在其他场景下,如果一个线程拿到锁了,但是因为没有实际来执行代码,就会不停的解锁、加锁,这样其他线程就饿死了,所以,还是在while里面,把里面的操作给上锁,这样看着没那么膈应。
如图的代码是最终版本的,上面的代码都是最终版本的。
(2)当队列是空的,就要阻塞等待
如图:
(3)如果没到时间,就要等待到时在执行要执行的代码
没到时间,就要阻塞等待,等待时间是: 要行的时间 - 现在的时间,没有限制要等待的时间的话,就会一直循环,每次循环判断是不是到时间了,因为循环这个代码执行速度是很快的,这样就会盲等,虽然计算机是在忙,都是在瞎忙活,所以代码要写出这样子,如图:
以上的代码都是计时器完整的版本,都看到这了,点个赞再走吧,谢谢谢谢谢!!!