一、定时器简介
定时器:就相当于一个闹钟,当我们定的时间到了,那么就执行一些逻辑。
1.1 Timer类
Java的标准库中提供了在java.util包下的Timer类作为定时器。
有如下的构造方法:
四种:
- timer() 无参构造;
- timer(boolean isDaemon) 创建的线程都是后台线程;
- timer(String name) 给定时器中创建的线程名字;
- timer(String name, boolean isDaemon) 创建的线程都是后台线程,也给定时器中创建的线程名字。
在Timer类中的核心方法是schedule方法。
- schedule(Timer task, Date time) 到达time时刻后执行task任务;
- schedule(Timer task, Date firstTime, long period) 到达time时刻后重复执行task任务,每次相隔period时间;
- schedule(Timer task, long delay) 在delay时间后执行task任务;
- schedule(Timer task, long delay, long period) 在delay时间后重复执行task任务,每次相隔period时间;
- scheduleAtFixedRate(Timer task, Date firstTime, long period) 到达time时刻后重复执行task任务,每次执行period时间;
- scheduleAtFixedRate(Timer task, long delay, long period) 在delay时间后重复执行task任务,每次执行period时间;
schedule的第一个参数是TimerTask类,这是一个实现了Runnable接口的抽象类。
1.2 使用案例
我们使用schedule方法来打印不同时间执行不同内容。
import java.util.Timer;
import java.util.TimerTask;
public class Demo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3000ms后执行");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("1000ms后执行");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("2000ms后执行");
}
},2000);
}
}
结果如下:会按照等待时间由小到大打印内容,并且执行完之后并不会结束,这是因为这些线程是前台线程。
二、实现简易定时器
自己实现的定时器主要要考虑下面几个内容:
- 设计一个类表示任务,对应TimerTask类;
- 使用优先级队列来组织多个任务,每次根节点都是等待时间最短的任务;
- 实现schedule方法,把任务添加到队列中;
- 额外创建一个线程,负责执行队列中的任务,根据时间来执行(即判断是否到了该执行的时间了)。
2.1 MyTimerTask类
这个类中需要:
- 将要执行的任务,和任务要执行的时刻记录下来,
- 并且这个任务还要有通过时刻比较得方法(即实现Comparator接口,重写CompareTo方法),便于后面存储进优先级队列。
代码:
class MyTimerTask implements Comparable<MyTimerTask>{
//记录任务
private Runnable task = null;
//记录执行任务的时刻
private long current = 0;
public MyTimerTask(Runnable task, long current) {
this.task = task;
this.current = current;
}
public Runnable getTask() {
return task;
}
public long getCurrent() {
return current;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.current - o.current);
}
}
2.2 实现schedule方法
我们实现schedule方法:
- 只需要将当前的任务传入队列中即可。
- 将参数Runnable的任务和时刻用来创建MyTimerTask类,在入队即可。
- 我们还要使用notify为后面的线程中因为队列为空调用wait进入阻塞状态提供唤醒。
代码:
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public void schedule(Runnable task, long delay) {
synchronized (this) {
MyTimerTask myTimerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
queue.offer(myTimerTask);
this.notify();
}
}
2.3 构造方法
在构造方法中额外创建一个线程,负责执行队列中的任务,根据时间来执行(即判断是否到了该执行的时间了)。
- 我们在最外层使用一层死循环来不断去读取队列中的任务。
- 如果队列空了,那么我们就出这次循环,但是如果使用continue的话,还是会在循环的去判断直到队列不为空为止。这样的消耗很高,我们可以使用wait等待schedule方法入队列后;来唤醒这个线程。
- 如果没有到达执行时间,我们也要出这次循环,但是使用continue也会导致在从现在这个时刻到执行时刻之间一直进行无意义的执行上面的代码,消耗很高,我们这里直接使用带参数的wait方法等待还需要的时间即可。
- 到达执行时间直接执行任务并出队列即可。
- 最后不要忘记启动这个线程。
代码:
public MyTimer() {
Thread thread = new Thread(()-> {
try {
while(true) { //循环拿任务,直到任务队列为空
synchronized (this) {
while (queue.isEmpty()) { //任务队列为空
this.wait();
}
MyTimerTask task = queue.peek();
if(task.getCurrent() > System.currentTimeMillis()) { //没到执行时间
this.wait(task.getCurrent() - System.currentTimeMillis());
} else {
task.run();
queue.poll();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
2.4 总代码
总代码如下:
class MyTimerTask implements Comparable<MyTimerTask>{
//记录任务
private Runnable task = null;
//记录执行任务的时刻
private long current = 0;
public MyTimerTask(Runnable task, long current) {
this.task = task;
this.current = current;
}
public Runnable getTask() {
return task;
}
public long getCurrent() {
return current;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.current - o.current);
}
public void run() {
task.run();
}
}
class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public void schedule(Runnable task, long delay) {
synchronized (this) {
MyTimerTask myTimerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
queue.offer(myTimerTask);
this.notify();
}
}
public MyTimer() {
Thread thread = new Thread(()-> {
try {
while(true) { //循环拿任务,直到任务队列为空
synchronized (this) {
while (queue.isEmpty()) { //任务队列为空
this.wait();
}
MyTimerTask task = queue.peek();
if(task.getCurrent() > System.currentTimeMillis()) { //没到执行时间
this.wait(task.getCurrent() - System.currentTimeMillis());
} else {
task.run();
queue.poll();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
2.5 测试
如果在main中执行下面这样的代码,也使用schedule方法来打印不同时间执行不同内容,会与上面使用案例的结果一样。
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3000ms后执行");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1000ms后执行");
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2000ms后执行");
}
},2000);
}
结果如下:会按照等待时间由小到大打印内容,并且执行完之后并不会结束,这是因为这些线程是前台线程。