在生活当中,有很多事情,我们不是立马就去做,而是在规定了时间之后,在到该时间时,再去执行,比如:闹钟、定时关机等等,在程序的世界中,有些代码也不是立刻执行,那么我们该如何实现呢?一探究竟——>《定时器》
1. 定时器
定时器是什么
定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执行某个指定好的代码.
定时器是⼀种实际开发中非常常用的组件.
比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连.
比如⼀个Map,希望里面的某个key在3s之后过期(自动删除).
类似于这样的场景就需要用到定时器.
在Java当中也给我们提供了定时器(Timer)的类,请见下文。
标准库中的定时器
- 标准库中提供了一个Timer类.Timer类的核心方法为
schedule
schedule
包含两个参数.第⼀个参数指定即将要执行的任务代码,第⼆个参数指定多长时间之后执行(单位为毫秒).
2. 自我实现一个定时器
1.首先定时器是用于处理任务的,我们该如何在定时器当中管理任务呢??
我们通过一个类,描述任务和任务执行的时间
具体任务的逻辑用Runnble表示,执行时间的可以用一个long型delay去表示
/*** 任务类*///由于需要比较时间大小,所以使用接口
class MyTask implements Comparable<MyTask>{//任务private Runnable runnable = null;//延迟时间private long time = 0;public MyTask(Runnable runnable, long delay) {//任务不能为空if(runnable==null){throw new RuntimeException("任务不能为空...");}//时间不能为负数if(delay<0){throw new RuntimeException("时间不能为负数...");}this.runnable = runnable;// 计算出任务执行的具体时间this.time = delay+System.currentTimeMillis();}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}//比较当前任务和其他任务的时间@Overridepublic int compareTo(MyTask o) {return (int) (o.getTime()-this.getTime());}
}
2.通过MyTask描述了任务之后,由于任务的执行顺序不一样,我们该如何去管理任务呢?
我们通过一个优先级队列把任务的对象组织起来
//用阻塞队列来管理任务
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
3.我们描述完了任务也通过优先级队列管理了任务对象,我们如何让任务对象和定时器关联起来呢?
/*** 添加任务的方法* @param runnable 任务* @param delay 延时* @throws InterruptedException*/public void schedule(Runnable runnable, long delay) throws InterruptedException {// 根据传处的参数,构造一个MyTaskMyTask task=new MyTask(runnable,delay);// 把任务放入阻塞队列queue.put(task);}
}
4.我们通过schedule方法把任务对象添加到了阻塞队列当中,我们只需要创建一个线程来执行任务即可
此时我们的思路是:创建一个线程不停的扫描任务,取出队列的首元素若时间到就取出执行,时间没到就放回队列不执行,就能写出以下代码:
// 创建扫描线程Thread thread=new Thread(()->{//不断的扫描队列中的任务while (true){try {//1.从阻塞队列中获取任务MyTask task = queue.take();//2.判断到没到执行时间long currentTime=System.currentTimeMillis();if(currentTime>=task.getTime()){//时间到了就执行任务task.getRunnable().run();}else {// 没有到时间,重新放回队列queue.put(task);}}catch (InterruptedException e) {e.printStackTrace();}}},"scanThread");//启动线程thread.start();
但是上面的代码有一个很明显的问题,就是 “忙等” ,为什么呢?
那么我们怎么解决这个忙等这个问题呢?
在放回队列时让程序等待一段时间等待一段时间
时间为:下一个任务的执行时间和当前时间的差
那么既然要等待了我们必须要通过持有同一个锁,来完成等待操作,所以我们创建一把锁
修改代码如下:
// 创建扫描线程
Thread thread=new Thread(()->{//不断的扫描队列中的任务while (true){try {//1.从阻塞队列中获取任务MyTask task = queue.take();//2.判断到没到执行任务的时间long currentTime=System.currentTimeMillis();if(currentTime>=task.getTime()){//时间到了就执行任务task.getRunnable().run();}else {// 当前时间与任务执行时间的差long waitTime = task.getTime() - currentTime;// 没有到时间,重新放回队列queue.put(task);synchronized (locker){//等时间locker.wait(waitTime);}}}catch (InterruptedException e) {e.printStackTrace();}}},"scanThread");//启动线程,真正去系统中申请资源thread.start();
通过锁,解决了忙等问题,
5.此时还有一个新的问题,在该队列中若产生了新的任务执行时间在等待任务之前该怎么办呢?
我们在每一次向阻塞队列当中添加新任务时,我们就唤醒一次扫描线程即可
/*** 添加任务的方法* @param runnable 任务* @param delay 延时* @throws InterruptedException*/public void schedule(Runnable runnable, long delay) throws InterruptedException {// 根据传处的参数,构造一个MyTaskMyTask task=new MyTask(runnable,delay);// 把任务放入阻塞队列queue.put(task);//在每次添加新任务时,唤醒一次扫描线程,以访扫描线程还在等待,新任务时间过了的问题synchronized (locker){locker.notifyAll();}}
}
6.CPU调度的过程中可能会产生执行顺序的问题,或当一个线程执行到一半的时间被调度走的现象,会引发什么问题呢?
造成该现象的原因是没有保证原子性,我们扩大锁范围即可解决该问题,修改后的代码如下:
//不断的扫描队列中的任务while (true){synchronized (locker){try {//1.从阻塞队列中获取任务MyTask task = queue.take();//2.判断到没到执行任务的时间long currentTime=System.currentTimeMillis();if(currentTime>=task.getTime()){//时间到了就执行任务task.getRunnable().run();}else {// 当前时间与任务执行时间的差long waitTime = task.getTime() - currentTime;// 没有到时间,重新放回队列queue.put(task);locker.wait(waitTime);}}catch (InterruptedException e) {e.printStackTrace();}}}
7.由于进入锁之后,MyTask task = queue.take();操作,当阻塞队列中没有元素时,就会阻塞等待,直到队列中有可用元素才继续执行,但是由于MyTask task = queue.take();操作持有了锁,导致无法释放锁,添加任务的方法又迟迟取不到锁,导致一个在等着任务执行,一个在等着获取锁添加任务,造成了
“死锁”
现象,我们该如何解决呢?
我们发现在为了解决原子性问题时,我们扩大加锁的范围,却又引入了更大的问题
一般我们两害相全取其轻
为了解决无法及时执行任务的问题,我们创建了一个后台的扫描线程,只做定时唤醒操作,定时1s或者任意时间唤醒执行一次
完整的定时器实现代码如下:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;/*** 自我实现定时器*/
public class MyTimer {//用阻塞队列来管理任务private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();//创建⼀个锁对象private Object locker = new Object();public MyTimer() throws InterruptedException {// 创建扫描线程Thread thread=new Thread(()->{//不断的扫描队列中的任务while (true){synchronized (locker){try {//1.从阻塞队列中获取任务MyTask task = queue.take();//2.判断到没到执行任务的时间long currentTime=System.currentTimeMillis();if(currentTime>=task.getTime()){//时间到了就执行任务task.getRunnable().run();}else {// 当前时间与任务执行时间的差long waitTime = task.getTime() - currentTime;// 没有到时间,重新放回队列queue.put(task);locker.wait(waitTime);}}catch (InterruptedException e) {e.printStackTrace();}}}},"scanThread");//启动线程,真正去系统中申请资源thread.start();//创建一个后台线程Thread daemonThread= new Thread(()->{while (true){//定时唤醒synchronized (locker){locker.notifyAll();}//休眠一会try {TimeUnit.MICROSECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});//设置成后台线程daemonThread.setDaemon(true);//启动线程daemonThread.start();}/*** 添加任务的方法* @param runnable 任务* @param delay 延时* @throws InterruptedException*/public void schedule(Runnable runnable, long delay) throws InterruptedException {// 根据传处的参数,构造一个MyTaskMyTask task=new MyTask(runnable,delay);// 把任务放入阻塞队列queue.put(task);synchronized (locker){locker.notifyAll();}}
}/*** 任务类*/
class MyTask implements Comparable<MyTask>{//任务private Runnable runnable = null;//延迟时间private long time = 0;public MyTask(Runnable runnable, long delay) {//任务不能为空if(runnable==null){throw new RuntimeException("任务不能为空...");}//时间不能为负数if(delay<0){throw new RuntimeException("时间不能为负数...");}this.runnable = runnable;// 计算出任务执行的具体时间this.time = delay+System.currentTimeMillis();}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}@Overridepublic int compareTo(MyTask o) {return (int) (o.getTime()-this.getTime());}
}