1.定时器是什么
定时器是软件开发中的一个重要组件,功能是当达到一个特定的时间后,就执行某个指定好的代码
定时器是一个非常常用的组件,特别是在网络编程中,当出现了"连接不上,卡了"的情况,就使用定时器做一些操作来止损
标准库中也提供了定时器
标准库中的Timer类
标准库提供了一个Timer类,Timer类的核心方法为schedule(安排,预定;将……列入计划表或清单)
schedule包含两个参数
第一个参数指定即将要执行的任务代码
第二个参数指定多多长时间之后执行(单位ms)
下面是用一下定时器
可以看到有两个参数
TimerTask类就是一个实现了Runnable接口的类,来描述指定的任务
delay是指定的时间后执行任务!
运行程序,经过指定的时间后,执行了run()中的语句
2.实现定时器
定时器的核心
注册任务后需要保证任务在指定的时间要被执行
单独在定时器内部,创建一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了就执行,没到就继续等待
一个定时器能连续注册N个任务,N个任务是按照最初约定的时间按顺序执行
这N个任务肯定需要一种数据结构来保存,不难发现,我们可以使用优先级队列,我们每个任务都是带有时间的,按照时间小的作为优先级高的,此时队首元素一定是最先要执行的任务,这时候扫描线程也只需要扫描队首元素即可,不必扫描整个队.如果队首元素没有到执行时间,那么其它元素也不可能到达执行时间!!
简而言之,定时器的核心:
1.有一个扫描线程,判断是否到执行时间.
2.还得有一个数据结构保存被注册的任务.
此处优先级队列是在多线程环境下使用的,因此要关注线程安全问题!自己手动加锁,或者使用标准库提供的PriorityBlockingQueue,它既有优先级又符合线程安全的要求
实现代码
我们先创建一个任务类
class MyTask{
//任务内容
private Runnable runnable;
//任务指定的时间(ms时间戳表示)
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
}
//获取时间
public long getTime() {
return time;
}
//执行任务
public void run(){
runnable.run();
}
队列里的"任务"使用Runnable表示,描述的是任务的内容
使用时间戳描述任务什么时候被执行
然后创建一个定时器
class MyTimer{
//扫描线程
private Thread t = null;
//阻塞优先级队列来保存任务
private PriorityBlockingQueue<MyTask> queue =
new PriorityBlockingQueue<>();
}
我们要给定时器类提供一个"schedule"方法来注册任务
//指定两个参数,一个是任务内容,一个是多长时间后执行任务
public void schedule(Runnable runnable,long after){
//注意时间的换算
MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(myTask);
}
接下来要实现一个比较麻烦的操作,就是扫描线程的实现
public MyTimer(){
t = new Thread(()->{
while (true){
try {
//取出队首元素,检查是否到执行时间了
//如果到了,就执行
//如果没到,就放回队列
//如果没有元素证明没有任务,会阻塞等待
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
//没到点,不用执行
//到点了,开始执行
queue.put(myTask);
}else{
myTask.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
上述代码大致实现了扫描线程的功能,但是还存在两个问题
第一个问题,我们还要明确我们的任务优先级是怎样的,还没指定
此时我们如果测试:
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
因为两个任务的优先级关系还没用comparable来设置,或者单独实现一个比较器comparator
运行程序
第二个问题,如果队首任务是四点进行执行,在两点的时候,线程开始扫描,就会一直从队首取出检查,发现没到执行时间,又放回去,反反复复!!直到四点开始执行.我们使用优先级队列来存储的,放回元素,堆就会进行一次调整,将这个任务又调整至队首,下次取出,还是这个元素!
这个循环没有阻塞,会快速的进行循环,是没有意义的,占用了cpu资源.这种情况称为"忙等",我们要对代码进行调整,进行阻塞式等待,sleep,wait..
如果等待时间明确,我们使用sleep可行吗?
此处看似等待时间是明确的,但是我们可能任意时间会来一个新的任务调用schedule,注册任务,那么队首元素就换了,必须得扫描出来.
如果用sleep,那么在sleep过程中就可能注册新的任务,如果在队首元素执行的时间前就要执行新注册的任务,然而用的sleep,就会错过这个任务的执行了
因此使用wait()notify()更合适,使用wait()进行等待,如果有新任务调用schedule,就notify(),重新检查一次,计算等待的时间
并且,wait()还有个超时时间的版本,如果没有新任务,则最多等到队首元素的执行时间就自动唤醒了
这样改动之后,我们既不会一直重复无用操作,也不会错过执行新注册任务
线程安全问题
代码到这里,还有个线程安全问题
我们考虑一个极端情况
如果代码执行到wait之前,这个线程被调度走了,当线程又被调度执行时,接下来就要进行wait操作,它的wait时间是算好了的,比如curTime是13:00,getTime是14:00,即将会wait一个小时,但是还没执行wait.
在该线程被调度走的过程中,如果另一个线程调用了schedule,注册了一个13:30执行的任务,此时schedule会执行notify()将wait()唤醒,但是扫描线程的wait()还没有执行呢,所以notify并没有实际作用,虽然新任务插入到队列中了,也是在队首.但是这个线程紧接又执行wait()一个小时,错过了这次任务的执行时间13:30
这都是多线程随机调度产生的,take和wait操作并非是原子的,如果这个过程是原子的,给它加上锁,保证不会有新的任务过来,就解决问题了,换言之就是要保证每次notify时,确实都在wait!
我们将锁的粒度变大,保证take和wait操作是原子的,就不会出现线程安全问题了