定时器是实际开发中常用的组件,例如文章的定时发布,双11的准点抢购活动等。

下面我们来看一下Java标准库中的定时器。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
}

该定时器会在3秒之后输出“hello”。创建一个定时器需要用到Timer类中的核心方法schedule,该方法内有两个参数,一个表示要执行的任务,一个表示任务在多长时间后执行。

认识了标准库中的定时器后,我们可以自己来模拟实现一个定时器。

首先,描述一个任务。创建一个MyTask类,类中有两个属性:一个是执行的任务,一个是任务执行时间。这两个属性类似于标准库schedule方法内的两个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyTask implements Comparable<MyTask>{
//具体要干啥
private Runnable runnable;
//啥时候开始干
private long time;

public MyTask(Runnable runnable,long dalay){
this.runnable = runnable;
this.time=System.currentTimeMillis()+dalay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}

@Override
public int compareTo(MyTask o) {
//时间小的排在前面
return (int) (this.time-o.time);
}
}

接下来,组织一个任务类。如何组织任务类呢,我们这里用到优先级阻塞队列。每个任务的执行时间(指的是在多长时间后执行)不同,根据时间大小来排序,进而优先执行队头任务,因此需要优先级队列。

最后,我们还需要一个线程不断的去扫描到了时间的任务,然后执行这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyTimer{
//组织一个任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public void schedule(Runnable runnable,long delay){
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
synchronized (locker){
locker.notify();
}
}
//执行到时间的任务
public MyTimer(){
Thread t1 = new Thread(()->{
while(true){
try {
//取队首元素
MyTask task = queue.take();
long time=System.currentTimeMillis();
if(time<task.getTime()){
//时间还没到
queue.put(task);
synchronized (locker){
locker.wait(task.getTime()-time);
}
}else{
//到时间了,执行这个任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
}
}

模拟一个定时器,总共分三步。第一步,把冰箱打开描述一个任务,即要执行的任务和任务多长时间后开始执行。第二步,组织一个任务,这里用到了优先级阻塞队列。第三步,利用一个线程扫描任务,执行到时间的任务。

下面有两个问题需要注意。

  1. 任务类要放进优先级阻塞队列中,优先级阻塞队列根据时间先后进行排序。因此我们的任务类要实现Comparable<MyTask>接口,然后重写比较规则。
  2. 线程扫描任务,会从队头取元素,判断是否到时间了,如果没到,再放回队列。接着继续取元素……如果不加限制,它一直不停的扫描队首元素, 看看是否能执行这个任务,这样会大量消耗CPU。因此我们利用wait来使这个线程等待,时间到后再唤醒。此外当新加入一个任务后我们也需要用notify来唤醒扫描线程,因为可能该任务的时间更小,优先级更高,所以需要重新扫描任务队列。(这也就决定了必须用wait,而不能用sleep,因为sleep不能中途唤醒)