前言
本篇博客重点介绍定时器的简单实现,帮助理解其底层原理。关于JAVA工具类自带的定时器,只会简单介绍,详细使用参阅官方文档(下文中有官方文档的连接)。
一、什么是定时器
定时器的概念非常简单。
它在软件开发中应用非常广泛。它类似于一个提前设定好的闹钟,到达指定时间后,执行设定好的任务程序。比如在网络通信中,如果在一个时间范围内没有得到响应,那么重新尝试连接网络等。
二、Java标准库中的定时器
标准库中提供了Timer类,核心方法是schedule(用于安排任务给计时器),它有两个参数:
- TimerTask task:描述要执行的任务,实际是对Runnable的封装
- long delay:延迟多长时间执行 ms
如图(来自官方文档):
代码演示:
一个Timer实例只会创建一个线程(默认创建的前台线程),因此如果有多个schedule的任务,他们是串行执行的。关于Timer类的详细信息和使用请参阅oracle官方文档:Timer
如果有在多线程环境下使用计时器的需要,需要用到线程池中的ScheduledExecutorService.具体使用请参阅oracle官方文档:ScheduledExecutorService
三、设计实现一个简单的定时器
这里介绍的计时器,只包含一个线程,并且可能会出现任务阻塞(如果一个任务执行时间过长,后执行的任务可能延迟执行)
/**
* 我们会使用优先级队列来判断那个任务最紧急需要先完成,所以需要实现Compareble,
* 然后重写compareTo方法,用时间戳进行大小比较
* 优先级队列中,存放的是一个一个要执行的任务
*/
class MyTimerTask implements Comparable<MyTimerTask> {
//用绝对时间——时间戳来表示时间间隔
long time;
//要执行的任务
Runnable runnable;
//构造方法
public MyTimerTask(Runnable runnable, long delay) {
//执行时间等于当前时间+延迟时间大小
this.time = System.currentTimeMillis() + delay;
this.runnable = runnable;
}
//用于执行任务
public void run() {
runnable.run();
}
//获取需要执行的任务的执行时间,等一下有用
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
//创建一个小根堆,时间到的先执行
return (int) (this.time - o.time);
}
}
class MyTimer {
//把schedule的任务放到这里,然后去除优先级大的任务,判断时间,是否需要执行
PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//创建一个锁对象
private Object locker = new Object();
public void schedule(Runnable runnable, long delay) {
//涉及优先级队列的读写操作,需要加锁保证线程安全
synchronized (locker) {
//把runnable和delay封装到myTimerTask中
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
//放入优先级队列
queue.offer(myTimerTask);
//通知唤醒myTimer创建的线程
locker.notify();
}
}
//构造方法,一旦创建这个实例,开始在优先级队列里面循环获取方法执行(如果到达要执行的时机)
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
//下面涉及对优先级队列的读写操作,需要保证线程安全
synchronized (locker) {
//目前没有任务,就一直睡,直到schedule了新的任务
while (queue.isEmpty()) {
locker.wait();
}
//有了任务,先看看当前需不需要执行
MyTimerTask task=queue.peek();
//时间已经到了,或者超时了,就执行
if(System.currentTimeMillis()>=task.time){
task.run();
//一处执行完的任务
queue.poll();
}else{
//没到时间,就睡到任务执行的时候
locker.wait(task.time-System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
t.start();
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(() -> {
System.out.println("hello 1000");
}, 1000);
myTimer.schedule(() -> {
System.out.println("hello 2000");
}, 2000);
myTimer.schedule(() -> {
System.out.println("hello 3000");
}, 3000);
System.out.println("程序开始运行");
}
}
执行结果:
此程序需要注意的几个点:
1)不要把下面的while换成if语句:
2)不能使用Thread.slee()替换wait()
原因有2
1、Thread.sleep在睡眠的时候是不会释放锁的(抱着锁睡),也就是睡眠的时候,schedule操作是无法进行的,会造成死锁!
2、notify和wait本来就是搭配起来用,实现线程之间通信的。
如果在myTimer线程睡眠的时候,有新的任务进来,notify会重新唤醒线程,而Thread.sleep是没有这个功能的。