前言
在服务器开放中,客户端向服务器发送请求,等待服务器响应,但若因为某个故障,导致程序一直无响应,怎么办?这时总不可能让客户端一直都没有响应,一直等下去,很有可能程序就卡死了,所以为了应对此种情况,程序员就设置了一个定时器,若到在定时器规定的时间没有完成任务,会执行某一个动作,响应客户端;
Timer标准库中用法
标准库中也有定时器(如下图)
标准库中定时器这个类里有个schedule方法,就是用来安排在多长时间之后来执行安排好的任务,因此他有两个参数(如下图)
一个参数是要执行的任务,就是一个Runnable,需要继承TimerTask,重写run方法,从而指定要执行的任务; 另一个参数是等待时间(单位是毫秒),也即是经过多长时间后,执行任务;
一个定时器中,可以同时安排多个任务,并且执行完任务之后,进程并没有退出,Timer内部需要一组线程来执行任务,这里的线程才是“前台线程”,会影响进程的退出;
如下代码:(安排一个时间表)
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("12:00 唱歌~");
}
}, 2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("12:30 弹钢琴");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("13:00 弹吉他");
}
}, 4000);
System.out.println("开始玩音乐");
}
执行结果如下:
Timer模拟实现
一、描述一个任务
schedule方法的第一个参数便是任务,所以我们需要能够描述一个任务,怎么描述呢?这里的任务需要包含两个信息,一个是要干什么事(Runnable),另一个是要什么时候执行;(如下图)
分析:
知道了任务和要执行的时间,这样就够了吗?想象一下,刚刚提到,一个定时器可以同时安排多个任务,那么这多个任务必然会涉及到先后发生的问题,那么供大于求的时候也就需要涉及到排序,可是任务是个类,怎么排序呢?比较器——继承Comparable接口,重写compareTo方法;(如下图)
分析:
这里由于time是long类型,所以需要强制类型转换,还有一个问题:“这里的return到底是this.time - o.time 还是 o.time - this.time 呢?”这里最好不要背,比较比较抽象,返回规则不是那么直观,所以,你只需要随便写一个,运行程序试一下,这多试几次,不也就知道吗~
二、创建MyTimer类,让MyTimer管理多个任务
可以用ArrayList吗?刚刚咱提到当供大于求时任务存储需要按序存储,ArrayList显然时无序,不方便找到时间最小的任务,所以这里不难想到使用优先级队列,但由于schedule时有可能在多线程中调用的,并且优先级队列并不是线程安全的,所以可以使用优先级阻塞队列(BlockingQueue)来实现;
四、创建一个扫描线程
从队列中取元素,可以用一个“扫描线程”来不停的检查队首元素,检查时间是否到了,若时间到了,则取出队首元素,执行任务,若时间没到,则将队首元素放回;(如下图)
分析:
由于是阻塞队列,所以因该先取出任务,才能够判断时间是否已经到了,若时间还没到,就把任务继续放回队列
五、各类问题及解决办法
存在问题分析一:“忙等”
观察代码,不难发现当时间还没到的时候,队列会循环的将队首元素取出,然后放回,这个循环就是在忙等,CPU并没有空闲出来,因此这里的等待也就变的毫无意义;
注意这里不可以用sleep(),假设当前时间为9点,任务时间是10点,那么你将设置 sleep 的时间为一个小时,这样合理吗?看似合理,但在多线程的情况下,很有可能会有新的任务出现,若新的任务时间为9点半,那么新的任务就需要执行sleep到10点,那么就会导致新的任务无法在9点半的时候执行!
解决办法:
使用wait()就可以很好的解决上述问题;如下图
存在问题分析二:“notify()空唤醒”
假设当前时间为9点,而当前任务执行的时间为11点,试想如果schedule在t1线程刚执行完take,还没有到wait的时候又新增了一个任务,这个任务的执行时间为10点,这会发生什么?
首先notify会在t1线程还没有wait的时候先唤醒一次,就相当于啥也没做,但其实notify即使什么线程都没唤醒,并不会存在什么问题,但是严重的问题在后面!当扫描线程继续往下执行,发现时间还没到,就将刚刚take出来的任务又放回了阻塞队列,然后就进行了wait等待,这以等待便是2个小时,这意味着,刚才新来的任务需要在10点的时候执行,却被错过了!
解决办法:
刚才出现问题的原因实际上就是因为notify在take和wait之间执行的,现在只需要把扫描线程中的锁范围扩大,保证take和wait之间这段操作的原子性,这样,就可以避免notify在take和wait之间执行;
来分析一下具体过程,9点的时候来了一个任务,需要在11点执行,扫描线程会先拿到锁,然后take,(这时将一个新的任务放入了队列,这个任务的执行时间为10点),接着实现中间逻辑,发现没到时间,就将take出的元素放入队列(此时他已经不是队首元素了),最后到wait;在这个过程中,schedule线程会一直阻塞等待锁,当扫描线程执行了wait,释放了锁,这个时候需要在10点开始执行的任务才拿到锁,执行notify唤醒了正在wait的线程,于是,继续循环取出队首元素,而此时,取出的队首元素便是要在10点执行的那个任务;(如下图)
这样写,就已经基本没问题了;
最后可以思考这样一个问题:若这样写(如下图),会存在什么问题?
问题分析:“死锁”
假设当我new了一个刚刚写好的MyTimer,那么此时就会初始化并调用MyTimer构造方法,构造方法中会创建一个线程t1,并开始线程,那么就会执行run方法,此时线程t1拿到锁,程序进入run方法,接着进行take操作,但是阻塞队列中没有元素,因此阻塞队列就会阻塞等待,直到有任务放入队列中,此时,来了一个任务,通过myTimer.schedule(//任务...,//时间...),此时进入schedule方法,但是由于刚刚线程t1已经拿到锁,schedule也会阻塞等待,所以此时schedule无法将任务put到队列中,这时t1在阻塞等待,schedule也在阻塞等待,就出现了死锁;
所以一定要把put操作放在所外面!
模拟定时器代码
//任务
class MyTask implements Comparable<MyTask>{
//将要执行的任务
private Runnable runnable;
//多久后执行任务
private long time;
public MyTask(Runnable runnable, long time){
this.runnable = runnable;
this.time = time + System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
//计时器
class MyTimer {
//优先级阻塞队列来存放任务,保证时间最小的先出队
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
//创建一个线程,不断来扫描下一个任务
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(true){
synchronized(locker) {
try {
//取出队首元素
MyTask task = queue.take();
//获取当前时间
long nowTime = System.currentTimeMillis();
//若到时了就执行任务,若没到时就阻塞等待
if(nowTime >= task.getTime()){
task.getRunnable().run();
}else{
//先将取出的元素放回
queue.put(task);
locker.wait(task.getTime() - nowTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
}
//安排
public void schedule(Runnable runnable, long time) throws InterruptedException {
queue.put(new MyTask(runnable, time));
synchronized(locker) {
locker.notify();
}
}
}
//测试
public class Test {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务1");
}
}, 1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务2");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务3");
}
},3000);
System.out.println("开始执行任务");
}
}