目录
一、什么是定时器
二、Java当中的定时器
①schedule()方法:
②TimerTask
编辑
③delay
三、实现一个定时器
前提条件:
代码实现:
①确定一个“任务”(MyTask)的描述:
②schedule方法:
③需要一个计时器
属性:
构造方法:
存在问题分析1:忙等:
解决"忙等":
存在问题分析二:notify()相比于wait()提前唤醒
一、什么是定时器
给一个非常常见的场景:
当我们在玩游戏,与其他玩家进行对战的时候,往往会出现下面的一些场景:
我们点击了"vs"按键,进入了对战加载的页面。
此时,如果在系统加载的过程当中,一直卡顿在某个时刻,无法前行。那么,难道程序就一直僵持住,无法行动了吗?
并不是的,这个时候,后台会有一个"计时器“,如果加载的时间超过了设定的时间之后,就执行其他的任务。
因此,总结一下,定时器本质就是一个拥有延时执行任务功能的工具。当达到指定的时间之后,可以执行定时器内部的任务。
二、Java当中的定时器
Java当中就有一个定时器Timer.
①schedule()方法:
定时器当中的核心方法就是:schedule()方法。
这个方法,用于注册一个任务,并指定这个任务在调用schedule方法延时多长时间执行任务。
可以看到,这个方法有两个参数:TimerTask task和long delay:下面将分析一下这两个参数
②TimerTask
点进去这个类,可以看到这个是一个抽象类。
Task的中文翻译就是任务。同时,这个抽象类也实现了Runnable接口,并且也把Runnable()接口当中的run方法继承了
这说明TimeTask这个类描述了定时器需要执行的任务。
③delay
确定延时的时间。当传入一个数字参数的时候,传入的毫秒数为这个定时器需要在调用schedule()方法之后多久执行自己的任务
代码实现(采用匿名内部类的方式实现):
public static void main(String[] args) {
System.out.println("程序启动");
Timer timer=new Timer();
//schedule()方法是”安排“的意思
//给定时器制定一个”任务“
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时器的任务1");
}
},3000);
}
运行程序,可以看到,TimerTask当中的run方法在调用schedule方法之后的3000毫秒内被执行了:
三、实现一个定时器
场景描述,有3个线程,分别延时3000ms,2000ms,1000ms。
如果想让延时越少的线程越先执行,那么应该如何初始化线程呢?
首先,初始化延时3000ms任务的定时器,再初始化2000ms任务的定时器,最后初始化1000ms任务的定时器。
这样,就可以让延时最短的线程最先执行:
运行结果:
前提条件:
如果想自定义一个定时器来管理需要执行的任务,那么就需要下面两个条件:
①让注册的任务在指定的时间内被执行
②注册的多个任务按照最初计划的时间运行
详细描述:
A.需要一个数据结构,来保存一开始创建的N个任务。
B.我们这里指定的任务,都是带个"时间" 。即:多久之后才执行任务。
C.那么,就可以考虑使用优先级队列(PriorityQueue).
按照时间的大小,建立小根据。
时间越少的任务,优先级越高,越先执行。
代码实现:
①确定一个“任务”(MyTask)的描述:
封装两个属性,一个是Runnable接口的引用,另外一个是delay,用于确定任务执行的时间:
同时,也要描述出这个任务的"优先级"。因此要让这个"任务”实现comparable接口,拥有可以比较的能力:明确任务的优先级
class MyTask implements Comparable<MyTask>{
/**
* 需要执行的任务
*/
private Runnable runnable;
/**
* 任务在什么时候执行:时间戳+延时时间
*/
private long delay;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.delay = delay;
}
/**
* 获取延长时间
* 延长的时间@return
*/
public long getDelay() {
return delay;
}
/**
* 执行任务
*/
public void run(){
runnable.run();
}
/**
* 实现一个比较器,让
* 这个任务是可以比较的
* 待比较的对象@param o
* 确定小根堆的比较方式
* @return
*/
@Override
public int compareTo(MyTask o) {
//this比o小,那么返回小于0
//谁的时间小,谁先执行
return (int) (this.getDelay()-o.getDelay());
}
}
②schedule方法:
指定需要执行的任务,以及当前任务延迟多久执行:并且把任务存放到优先级阻塞队列(PriorityBlockingQueue当中)
/**
* 指定两个参数
* 第一个表示任务@param runnable
* 第二个表示多久之后执行@param after
*/
public void schedule(Runnable runnable,long after){
MyTask myTask=new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(myTask);
}
③需要一个计时器
属性:
其中一个是存放任务的优先级阻塞队列(PriorityBlockingQueue):
调用了schedule()方法之后传入的任务的优先队列
优先级高(等待时间少)的任务先执行
另外一个是扫描线程t。
扫描线程t的作用:
一般情况下面,一个定时器(Timer)只需要初始化一次,然后就可以存放多个任务到定时器当中。
扫描线程由于是在构造方法当中初始化的,那么,也就意味着扫描线程也只需要初始化一次。
它的作用就是不断从阻塞队列当中取出任务(Task),并且执行。
如果没有扫描线程,那么也就意味着一切的任务(Task)都需要由主线程来执行。
代码实现:
/**
* 扫描线程
* 扫描线程:用于在构造方法当中初始化,并且执行从阻塞队列当中循环取出任务的线程
*
*/
private Thread t;
/**
* 一个阻塞的优先队列,用于保存
* 调用了schedule()方法之后传入的任务的优先队列
* 优先级高(等待时间少)的任务先执行
*/
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
构造方法:
A.初始化扫描线程t,并且指定它对应的任务
B.在线程t的任务当中,需要不断循环从优先级的阻塞队列当中取出MyTask。
并且进行判断:
①当前时间没有达到任务需要执行的时间的时候,就把任务放回优先级阻塞队列当中。
②如果当前时间已经达到了任务需要执行的时间,那么就执行对应的任务,调用myTask.run()方法。
public MyTimer(){
t=new Thread(()->{
while (true){
//取出队首元素,检查看看队首元素任务是否时间到了
try {
MyTask myTask=queue.take();
//如果时间没有到,继续把任务放回到队列当中
//获取现在的时间
long curTime=System.currentTimeMillis();
//拿现在的时间和myTask需要执行指定的时间进行对比
//如果现在的时间没有到需要执行的时间,那么不执行任务,把这个任务放回到阻塞队列当中
if(curTime<myTask.getDelay()){
queue.put(myTask);
}else {
//如果到了需要执行的时间,那么执行对应的任务
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
存在问题分析1:忙等:
当前时刻,如果达到了任务需要执行的时间,那么的确可以执行任务。
如果没有达到任务需要执行的时间,那么,会不断循环地出现:
把任务(task)从阻塞队列当中取出来,然后再把任务(task)放到阻塞队列当中的现象。
由于我们这里使用的是优先级队列,因此,当把这个任务再次取出来的时候,仍然是堆顶的元素。
这样的话, 假如一个任务需要在14:00执行,但是现在的时刻为13:00,远远没有到达需要执行的时间,那么,在这1h之内,将执行数以10亿次的循环,并且这个循环都是没有意义的。
这种现象,在计算机当中,被称为"忙等"。也就是,等着任务执行,但是在等的这个过程当中,并没有"闲着”,而是不断重复没有意义的工作。
按道理来说,等待的过程需要释放CPU资源的,但是这里并没有释放,因此造成了CPU资源的浪费。
解决"忙等":
针对上述代码,不要再执行忙等了,而是"阻塞式“等待。
描述:
当任务从计时器当中被取出来的时候,如果没有到达任务需要执行的时间,那么就让扫描线程进入阻塞等待的状态,等待的时间为当前时间与线程规定的时间之差。
也就是说,如果当前任务没有到达执行的时间,那么就需要等待到执行的时间。
此处,可以考虑使用wait(timeout)或者sleep(timeout)两种方式来进行阻塞等待。那使用哪种方式比较好呢?
使用wait()方法比较好,原因如下:
如果使用了Thread.sleep(timeout)方法,那么也就意味着,扫描线程需要等待到timeout时间了,才可以重新工作。
如果在这个期间,其他任务(MyTask)也被添加到了计时器当中,那么,它们将无法在timeout时间之前执行。
因为,所有添加的任务执行,都是需要通过扫描线程来执行的。既然扫描线程都要wait()到对应的时间执行,那么新添加的任务,也不能执行了。
因此,需要使用wait(timeout)方法来代替Thread.sleep(timeout)来执行对应的任务。
当定时器当中有新的任务添加的时候,把扫描线程notify()了。
代码实现:
存在问题分析二:notify()相比于wait()提前唤醒
假如现在的时间为13:00
在上图的代码当中,定时器存放了两个任务,其中一个任务是延时3000秒执行,另外一个是延时2000秒执行。
假如,这一个定时器被创建好之后。某一时刻,扫描线程从定时器当中取出一个任务(MyTask)
当取出来之后(也就是红色箭头指向的部分),读取到了任务的执行时间为14:00,被判定为没有到指定的执行时间。
但是,此时扫描线程被操作系统调度离开了cpu内核。
此时,又有一个新的线程,调用了schedule()方法,往定时器当中存放任务。指定任务的执行时间为13:30。
根据上面的写法,新的线程在执行schedule()方法快结束的时候,会notify()唤醒正在等待的扫描线程。
图解:
时间轴 | 扫描线程 | 添加任务的线程 |
t1 | 取出任务 | |
t2 | 判定时间,没有到达指定的时间 | |
t3 | 被调度离开cpu内核 | |
t4 | 调用schedule()方法添加任务到阻塞队列当中,并且notify() | |
t5 | 回到cpu内核,并且执行wait(timeout) |
可以看到,新添加任务的线程,比扫描线程先执行了notify()方法。
那么,也就意味着,t5时刻,执行的等待时间,是从13:00开始的,需要等待1h。那么,新添加的任务,也就是需要在14:30执行的任务,无法正常执行了。
那如何解决这个问题,也就是让wait()方法优先执行呢?
那就是,让"取出任务","判定时间","执行wait()"这三个操作变成原子的,保证take()和wait()操作之间不要有其他任务"贸然闯入"。
图解:
时间轴 | 扫描线程 | 添加任务的线程 |
t1 | 取出任务 | |
t2 | 判定时间,没有到达指定的时间 | |
t3 | 进行等待(timeout),wait() | |
t4 | 添加任务 | |
t5 | 唤醒扫描线程:notify() |
代码实现: