定时器就是闹钟的效果,指定要一个任务(runnable),指定一个时间,此时这个任务不会立马去执行,而是时间到了才会去执行,这个过程称为——定时执行/延时执行。
日常开发中定时执行是一个非常重要的开发组件,比如说短信的验证码是有时效的,这样的效果就可以使用定时器:发送验证码的时候保存一份验证码,当过了规定时间就删除这个验证码。
标准库的Timer
Java标准库的定时器——Timer类
首先实例化一个timer类,然后通过实例对象调用schedule方法可以实现上述操作,可以看到这个方法有两个参数:
第一个参数TimerTask,当我们点进去它的源码可以看到它其实是实现了Runnable接口的,所以就当作runnable使用就可以了。
第二个参数long delay表示“多长时间后执行”,以当前执行schedule的时间为基准,再等delay的时间后进一步执行。
写一段代码感受一下定时执行:
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("delay 3000");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("delay 2000");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("delay 1000");
}
},1000);
}
写了三段分为delay3~1秒,所以输出顺序是从1000-》3000
可以注意到这个时候任务都执行完了但是进程并没有结束,是因为Timer内部包含了前台线程,组织了进程的结束。
自己实现定时器
真正的学会一个集合框架/类方法的顺序大概为:了解用法->理解原函数的原理->自己动手实现类似的类,所以自己实现一个类方法是特别有用的,可以加深我们对这个类方法的了解。
在实现前应该先构思好自己实现的定时器的框架,我们的需求是:(1)能够实现定时执行的效果 (2)能够管理多任务;
第一步:首先我们需要有一个类来表示任务,任务类中要保存执行任务的绝对时间,方便后面线程执行的时候方便判定是否要执行该任务。
class MyTimerTask{
public Runnable runnable;
public long time;
public MyTimerTask(Runnable runnable,long time){
this.runnable = runnable;
this.time = time + System.currentTimeMillis();
}
void run(){
runnable.run();
}
}
上面构造方法中成员变量time的赋值是传入的需要等待的时间加上当前的时间,也就是将执行的绝对时间赋值了,run方法就是来执行当前任务的方法。
第二步:通过数据结构保存多个任务。比较直观的是使用List来保存多个任务,但如果list中的元素也就是任务较多时就要频繁的遍历每一个任务来看是否到了执行时间,我们想要的功能是可以每次访问等待时间最短的元素,如果这个时间最短元素没到时间那么别的肯定也不会到时间,所以可以使用优先级队列创建小根堆实现是最优解。
把这些任务保存到优先级队列,按时间的顺序来排,可以做到队首元素就是时间最短的元素。我们想要的比较大小方式是按时间排序,所以可以实现comparable接口的方式重写比较方法,改为按照时间来排。加上第二步逻辑框架的代码后:
class MyTimerTask implements Comparable<MyTimerTask>{
public Runnable runnable;
public long time;
public MyTimerTask(Runnable runnable,long time){
this.runnable = runnable;
this.time = time + System.currentTimeMillis();
}
void run(){
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(o.time - this.time);
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
}
第三步:要有一个线程去执行这里的任务,在构造方法中创建一个线程去执行队列中的任务
先创建好框架,我们再进行逐步优化:
先获取队列中的第一个元素看是否到达执行时间,到了就run()然后将该任务从队列中删除,没到就继续循环判定。
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public MyTimer(){
Thread t = new Thread(()->{
while (true){
MyTimerTask task = queue.peek();
if(task.time <= System.currentTimeMillis() ){
task.run();
queue.poll();
}else {
continue;
}
}
});
}
}
第四步:创建一个方法来将所有任务添加进队列
public void MySchedule(Runnable runnable,long time){
MyTimerTask task = new MyTimerTask(runnable,time);
queue.offer(task);
}
第五步:基础框架已经搭建好,接下来进行优化操作:(1)引入锁操作来保证线程安全,将线程中的关键操作加锁 (2)原本的代码中查询队首元素没到执行时间后会继续频繁的循环来检测直到执行时间,是非常消耗资源的,所以在检测没到时间时可以使用wait等待剩余的时间 (3)当任务队列为空时也wait阻塞等待,在队列加入元素后进行notify唤醒。
此处为什么使用wait而不是sleep?
(1)使用sleep的话睡了就真睡了,如果通过interrupt唤醒属于非常规操作 (2)sleep不会释放锁,会影响后续插入操作。
加上刚才的优化操作后整个自己实现的定时器类如下:
class MyTimerTask implements Comparable<MyTimerTask>{
public Runnable runnable;
public long time;
public MyTimerTask(Runnable runnable,long time){
this.runnable = runnable;
this.time = time + System.currentTimeMillis();
}
void run(){
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(o.time - this.time);
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
Object locker = new Object();
public MyTimer(){
Thread t = new Thread(()-> {
try {
while (true) {
synchronized (locker){
if (queue.isEmpty()) {
locker.wait();
}
MyTimerTask task = queue.peek();
if (task.time <= System.currentTimeMillis()) {
task.run();
queue.poll();
} else {
locker.wait(task.time - System.currentTimeMillis());
}
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
}
public void MySchedule(Runnable runnable,long time){
MyTimerTask task = new MyTimerTask(runnable,time);
queue.offer(task);
locker.notify();
}
}
多线程到这就结束了,有对多线程感兴趣的朋友可以看看前几期多线程的内容,感谢观看。
感谢观看
道阻且长,行则将至