在日常生活中,如果我们想要在 t 时间 后去做一件重要的事情,那么为了防止忘记,我们就可以使用闹钟的计时器功能,它会在 t 时间后执行任务(响铃)提醒我们去执行这件事情. — 这就是Java定时器的简单功能。它作为一种日常开发组件。约定一个时间,时间到达之后,执行某个任务。常被用于网络通信。
也比如在客户端和服务器之间,当客户端发出去请求之后,服务器就要返回响应,客户端这边要等待响应,而网络环境是复杂的,如果等待时间较长,这个原因是啥,是请求没法送过去?响应丢了?还是服务器出问题了。对于客户端来说,不能无限的等,需要先设置一个最大的期限。这时"等待最大期限"就可以通过定时器的方式实现了。
Java中的定时器的类是 : Timer ,为util包中的一个无继承关系的类, 从该类的构造方法中,我们可以使用无参构造器创建该类的对象,也可以在创建类对象的时候指定定时器中所需要的线程的名字,与是否为守护进程。
- 在定时器中最常用的方法就是 schedule(TimerTask task, long delay)
- 该方法传参的是一个 TimerTask 对象,与定时器约定的执行时间间隔 delay
- 包:import java.util.Timer;
而 TimerTask 类则是一个来描述计时器任务的类,该类中有 抽象方法 run(),并且该类是实现了runnable接口的,所以我们给 schedule 传参中的 TimerTask 对象都要重写 run() 方法,重写的run() 方法中的语句,则是定时器需要执行的语句。
而 delay 则是我们约定从当前时间后的 delay 内执行传入的任务.时间单位为 毫秒。
接下来我们来看一个简单的定时器的使用 :
我们在创建定时器的时候指定了定时器中的扫描线程的线程名,然后使用 schedule 方法传入任务与任务执行的间隔时间 1000 毫秒
这个时候在执行该代码的1000毫秒后,定时器就会将该任务执行。
import java.util.Timer;
import java.util.TimerTask;
public class demo1 {
public static void main(String[] args) {
Timer timer = new Timer("线程1");
timer.schedule(new TimerTask() { //使用匿名内部类继承TImerTask类
@Override
public void run() { //TimerTask类实现了Runnable接口要重写run方法
System.out.println("执行任务");
}
},1000); //delay相对时间 任务执行时间
System.out.println("程序启动!");
}
}
主线程执行schedule方法的时候,就是把这个任务放到timer对象中了。并且timer里面也包含一个线程(扫描线程),时间一到,扫描线程就会执行刚才安排的任务了。
可以发现,程序运行完,整个程序并没有结束。正是因为TImer里的线程,阻止了线程结束!
利用 jconsole 观察该线程处于 WAITING 状态:
//描述一个任务的类
public class MyTimerTask implements Comparable{
//要有一个任务
private Runnable runnable;
//要有一个时间
private long time;
//构造方法 传入任务和时间
public MyTimerTask(Runnable runnable,long delay) {
//任务
this.runnable = runnable;
//任务发生时间
this.time = System.currentTimeMillis()+delay;
}
//为外部提供获取任务发生时间
public long getTaskTime() {
return this.time;
}
//为外部提供获取任务
public Runnable getRunnable() {
return this.runnable;
}
//重写比较方法
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
}
实现Comparable接口是因为数据结构我们用到了优先级队列,需要重写比较方法,重新定义比较规则。有两种方法,一种是实现Comparable接口,另一种是比较器Comparator接口,用内部类实现。
import java.util.PriorityQueue;
//定时器 即指定几分钟后或其他时间后干什么
//定时器
public class MyTimer {
//优先级队列 使用比较器 匿名内部类
//private PriorityQueue queue = new PriorityQueue<>(new Comparator() {
// @Override
// public int compare(MyTimerTask o1, MyTimerTask o2) {
// return (int) (o1.getTaskTime()-o2.getTaskTime());
// }
//});
//优先级队列
private PriorityQueue queue = new PriorityQueue<>();
//多个线程针对同一个对象上锁 锁对象
private Object locker = new Object();
public void schedule(Runnable runnable,long delay) {
//线程不安全
synchronized (locker) {
//添加任务及任务时间
queue.offer(new MyTimerTask(runnable,delay));
//唤醒队列
locker.notify();
}
}
//扫描线程
public MyTimer() {
//创建一个扫描线程
Thread t1 = new Thread(()->{
//不停地扫描队列 即队头 查看是否到达时间
//有可能下一次新添加的任务的时间更短 队头改变
while(true) {
synchronized (locker) {
while (queue.isEmpty()) {
try {
//队列为空 等待
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列不为空 有任务 下面进行时间比较
//获取当前任务
MyTimerTask task = queue.peek();
//获取当前时间 时间戳
long currTime = System.currentTimeMillis();
if(currTime>=task.getTaskTime()) {
//到任务时间 执行任务
task.getRunnable().run();
queue.poll();
}else{
//未到任务时间 也进行等待 降低扫描速度
try {
locker.wait(task.getTaskTime()-currTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
//启动线程
t1.start();
}
}
数据结构我们选择使用优先级队列,有啥好处?假如在选择数据结构之前,我们先假设使用数组ArrayList,此时扫描线程,就需要不停地遍历数组中的每个任务,判定每个任务是否到达执行时间。这样的遍历效率是非常低的。如果使用优先级队列,再重写比较方法,让整个任务由时间大小按照小根堆排列,那么最先执行的就是时间最小的任务了,时间复杂度将降到O(1),判定任务时间是否到达更高效。
在该类的构造方法中,我们还创建了一个线程,不断地对任务队列中优先级最高(最快执行)的任务进行查看, 看是否到达执行时间。
当队列为空时,线程进入阻塞等待,直到添加一个任务时,线程继续执行。
队列中有任务时,但当前时间最短的任务还未到达执行时间时,也进行阻塞等待。这里的阻塞等待是有参的,为执行时间与当前时间的差值。其实也完全可以不等待,继续循环扫描。此处阻塞的好处就是wait之后,就会释放锁,线程就不会在CPU上执行了,就可以把CPU资源让给其他线程使用了。
那么对于wait和sleep来说都是等待,为啥不用sleep?sleep是指定时间让线程进行休眠,假如在sleep的过程中,我添加了一个比之前队列中任务执行时间还早的任务,那么sleep就不能及时执行最新的这个任务。而我设计的代码中,若使用wait,每添加一个任务时,notify都会唤醒不管是因为空队列进入阻塞状态的线程,或者是因为未到达任务时间而阻塞等待的线程(两种阻塞不会同时出现),就算是添加了一个比之前队列中任务执行时间还早的任务,也能及时执行任务。
public class test {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1000");
}
},1000);
//System.out.println("00");
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
}
}
结果:
三个注意点: