目录
(一)定时器的作用
我们在实际的应用场景中总会需要让程序在固定的时间执行某个逻辑,比如说客户端给服务器发送了一个情求,等了三个小时还不见反应,这时我们难道要一直等下去吗?还是应该放弃等待或者执行其他的一些逻辑呢?还有就是比如我们在打王者荣耀的时候,每到10分钟就会刷新龙王,每隔几十秒就会刷新野怪,这也一定是用到了定时器的。
(二)标准库中的定时器
在Java标准库中,是提供给我们有一个定时器的类的叫Timer类,既然定时器是要求特定的事件完成特定的任务,那么定时器就必然要有两个要素
1:需要执行的任务
2:需要执行任务时的时间
Timer类中有一个schedule方法来实现定时任务,代码如下
public class Main {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {//此处使用了匿名内部类的写法
@Override
public void run() {//run方法描述定时器需要执行的任务
System.out.println("执行定时器任务3000");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时器任务6000");
}
},6000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时器任务9000");
}
},9000);
System.out.println("主线程执行任务");
}
}
其中描述定时器任务的run方法是TimerTask继承了Runnable接口实现的(如下图),因此可以意识到,定时器是牵扯到一个多线程的组件
运行代码结果
当主线程执行schedule的方法的时候,就是把这个任务放到timer对象中,于此同时,timer中包含一个”扫描线程“,一旦时间到达,扫描线程就会执行刚才安排的任务
再仔细观察运行结果图会发现,进程并没有结束,就是因为这个Timer内部的“扫描线程”还在扫描,所以进程还没结束。
(三)模拟实现定时器
为了更加深刻的理解定时器,我们来手搓一个定时器
既然我们要实现一个东西,我们就需要先分析一下这个东西。这就是发现问题,分析问题,解决问题三步
1:我们需要创建一个类,通过类的对象来描述所需要执行的任务(包含任务内容与时间)
2:还需要一个数据结构来保存这个类,并能达到按照时间远近排序的目的,这里用优先级队列
3:还需要有一个扫描线程,来时刻监视是否有满足条件的任务(时间到了),执行这个任务,并从数据结构中删除
(1)描述任务的类(MyTimerTask)
我们想这个描述任务的类需要什么,既然是描述任务,我们就看这个任务里有什么元素,1这个任务里面需要有一个任务的执行细节,2这个任务还需要有具体的执行时间,3这个任务需要实现comparable接口来保证能够在优先级队列中按照时间大小排序
如下代码,我们用Runnable描述任务,用time描述时间,并重写compareTo方法保证这个描述任务的类能够按照时间排序
class MyTimerTask implements Comparable<MyTimerTask>{
//描述任务
private long time;
private Runnable runnable;
public MyTimerTask(long time, Runnable runnable) {
this.time = time;
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {//重写比较方法,按照时间排序
return (int)(this.getTime()-o.time);
}
}
(2)存放任务的数据结构
至于为什么我们在这里使用优先级队列,最浅显的一个理由可以是,相比于其他数据结构,比如顺序表。
在数据存取的时间复杂度方面, 顺序表的插入删除时间复杂度都是O(n),而优先级队列的时间复杂度都是O(logn)。
在查找最高优先级方面,顺序表查找最高优先级的元素需要遍历整个表,时间复杂度同样为O(n),而优先级队列的堆顶元素始终是最高优先级元素,查找时时间复杂度时O(1),这是非常明显的优势
如下代码中,用优先级队列存放MyTimerTask,在schedule方法中,优先级队列将描述好的内容,存放进PriorityQueue中,注意在存放时间上,不是直接存放的延迟时间,存放的是现在时间加上延迟时间,那是未来某一需要执行任务时的时间点
注意:这里的代码还有很严重的线程安全问题,不是最终代码,在后面会进行解释
public class MyTimer3 {
//优先级队列保存MyTimerTask
PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();
public void schedule(Runnable runnable,long delay){
priorityQueue.offer(new MyTimerTask(delay+System.currentTimeMillis(),runnable));
}
}
(3)扫描满足任务的线程
我们拆解一下这个线程需要完成的任务
1:这个扫描线程需要在对象一创建的时候就运行
2:要持续监视优先级队列中是否为空,如果队列中为空则阻塞等待,直到队列中有可以执行的任务,再唤醒
有可以执行的任务后,需要判断任务是否到可运行时间,到了运行时间运行任务,并把任务从队列中删除,若未到时间,则阻塞等待
如下代码 ,在MyTimer3的构造函数中创建扫描线程,保证了线程能在对象一经创建就会运行,然后通过if else的判断来完成是否需要执行任务。
public class MyTimer3 {
//锁对象
Object locker = new Object();
//一个数据结构保存任务,这里用优先级队列
PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();
//有一个方法,用来把任务放入数据结构中
public void schedule(Runnable runnable,long delay){
synchronized (locker){
priorityQueue.offer(new MyTimerTask(delay+System.currentTimeMillis(),runnable));
locker.notify();
}
}
public MyTimer3() {
Thread thread = new Thread(() -> {
//扫描优先级队列里是否有符合条件的任务
while (true) {
synchronized (locker) {
//先判断队列中是否有任务
if (priorityQueue.peek() != null) {//如果有任务
//有任务后判断任务是否到时间
if (priorityQueue.peek().getTime() <= System.currentTimeMillis()) {
//如果到了时间执行任务,并抛出
priorityQueue.peek().getRunnable().run();
priorityQueue.poll();
} else {//否则阻塞等待
try {
locker.wait(priorityQueue.peek().getTime() - System.currentTimeMillis());//带参版本wait()防止忙等
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
} else {
//如果没有任务
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
thread.start();
}
}
扫描线程中,需要解决的线程安全问题
在上述代码中,扫描线程需要向优先级队列中读取任务并运行,而schedule方法需要向优先级队列中添加任务,这种多个线程操作一个队列的情况,是一个典型的线程安全问题,因此当队列为空时,通过locker.wait(),使读操作阻塞,当schedule方法向队列中添加任务后,再用locker.notify(),将读操作唤醒。
带参版本wait()解决的忙等问题
当需要执行的任务还没到执行时间,如果不做任何操作,while循环会一直进行检查看时间到没到,就像你六点半下班,可现在才六点,在这半小时中,你只是一直重复的看表,而没有休息或做其他事情一样,显然是不科学的 ,因此我们引入带参数版本的wait(),让线程在这段时间里阻塞等待,这样就防止了忙等的发生。
(4)完整代码(包含测试用例)
import java.util.PriorityQueue;
//先要有一个执行的任务,
// 还要有一个执行的时间,
//还要有一个优先级队列来存放任务
// 再要有一个扫描的线程来观察是否有满足要求的任务,如果有满足要求的任务就执行,否则不执行阻塞等待
public class MyTimer3 {
//锁对象
Object locker = new Object();
//一个数据结构保存任务,这里用优先级队列
PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();
//有一个方法,用来把任务放入数据结构中
public void schedule(Runnable runnable,long delay){
synchronized (locker){
priorityQueue.offer(new MyTimerTask(delay+System.currentTimeMillis(),runnable));
locker.notify();
}
}
public MyTimer3() {
Thread thread = new Thread(() -> {
//扫描优先级队列里是否有符合条件的任务
while (true) {
synchronized (locker) {
//先判断队列中是否有任务
if (priorityQueue.peek() != null) {//如果有任务
//有任务后判断任务是否到时间
if (priorityQueue.peek().getTime() <= System.currentTimeMillis()) {
//如果到了时间执行任务,并抛出
priorityQueue.peek().getRunnable().run();
priorityQueue.poll();
} else {//否则阻塞等待
try {
locker.wait(priorityQueue.peek().getTime() - System.currentTimeMillis());//需要搭配sychronized使用
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
} else {
//如果没有任务
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
thread.start();
}
}
class MyTimerTask implements Comparable<MyTimerTask>{
//描述任务
private long time;
private Runnable runnable;
public MyTimerTask(long time, Runnable runnable) {
this.time = time;
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.getTime()-o.time);
}
}
class Main{
public static void main(String[] args) {
MyTimer3 myTimer3 = new MyTimer3();
myTimer3.schedule(new Runnable() {
@Override
public void run() {
System.out.println("之星说的话覅");
}
},1000);
myTimer3.schedule(new Runnable() {
@Override
public void run() {
System.out.println("给i哦该啊");
}
},3000);
}
}