什么是定时器
定时器, 就类似于一个闹钟, 当设定的时间一到, 就执行某个指定的代码.
标准库中的定时器
Java 标准库中提供了一个 Timer 类, 其核心方法为 schedule.
方法 | 作用 |
void schedule(TimerTask task, long delay) | 到达指定的 delay (单位毫秒)时间后, 执行指定任务 task |
schedule 有两个参数, 第一个参数指定要执行的代码, 第二个参数指定时间, 时间单位为毫秒.
public static void main(String[] args) {
Timer time = new Timer();
time.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer 3000");
}
},3000);
}
定义一个 timer 可以添加多个任务, 当这个代码执行完, 发现进程并没结束, 这是因为 Timer 里内置了线程(前台线程), 它不知道你的代码是否还会添加新的任务进来, 有一个主动结束的方法 cancle()
实现定时器
首先需要有一个线程, 负责帮我们掐时间, 等时间到达, 这个线程就帮我们执行.
还需要一个队列/数组, 负责保存所有 schedule 进来的任务.
我们使用优先级队列来实现上述内容. 每个任务都带有时间, 肯定先执行时间小的, 再执行时间大的.
可以使用标准库中提供的 PriorityQueue (线程不安全) -----> 我们使用这个, 手动加锁.
标准库中也提供了 PriorityBlockingQueue (线程安全) -----> 这个不好控制, 容易出问题~
创建 MyTimerTask 类, 来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
//在什么时间点来执行任务
private long time;
//实际任务要执行的代码
private Runnable runnable;
//构造方法
//这里的 delay 是一个相对时间间隔
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//计算要执行任务的绝对时间
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
}
因为这些任务是要放到优先级队列里的, 所以应该是可比较的, 这里要实现 Comparable 接口, 重写compareTo() 方法, 至于谁减谁, 试一试就知道了.
创建 MyTimer 类, 表示一个定时器
class MyTimer {
//负责扫描任务队列, 执行任务的线程
private Thread t = null;
//任务队列
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public void schedule (Runnable runnable, long delay) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
}
//构造方法, 创建扫描线程, 让扫描线程来判定和执行
public MyTimer() {
t = new Thread(()->{
//扫描线程需要反复扫描队首元素, 并判断队首元素时间是不是到了
//如果时间没到, 啥也不干
//如果时间到了, 执行任务, 并删除它
while(true) {
if(queue.isEmpty()){
//先不做处理
continue;
}
MyTimerTask task = queue.peek();
//获取当前时间
long curTime = System.currentTimeMillis();
if(curTime >= task.getTime()) {
//时间到了, 执行并删除它
task.run();
queue.poll();
} else {
//时间没到,暂时先不执行
continue;
}
}
});
}
}
加锁
首先创建一个锁对象
public Object locker = new Object();
引入锁的目的 是保证队列在多线程下是安全的, 所以把有关队列的操作都要加锁.
然后就是阻塞等待了, 当队列为空, 需要阻塞等待, 那什么时候唤醒呢?
肯定是队列不为空啦~
当判断任务时间还没有到达, 也是需要等待的, 不过这里可不是盲等, 要有最大等待时间.
这个时间怎么算呢? 就是用该任务设定的绝对时间减去当前时间就可以啦~
locker.wait(task.getTime() - curTime);
要注意, 这里不能用 sleep .
如果后续有任务进来, 但又没有解锁, 那就会错过这个任务. 而且一直不解锁, 占用资源.
完整代码:
//描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
//在什么时间点来执行任务
private long time;
//实际任务要执行的代码
private Runnable runnable;
//构造方法
//这里的 delay 是一个相对时间间隔
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//计算要执行任务的绝对时间
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer {
//负责扫描任务队列, 执行任务的线程
private Thread t = null;
public Object locker = new Object();
//任务队列
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public void schedule (Runnable runnable, long delay) {
synchronized (locker){
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
//添加元素之后, 队列不为空, 唤醒
locker.notify();
}
}
//构造方法, 创建扫描线程, 让扫描线程来判定和执行
public MyTimer() {
t = new Thread(()->{
//扫描线程需要反复扫描队首元素, 并判断队首元素时间是不是到了
//如果时间没到, 啥也不干
//如果时间到了, 执行任务, 并删除它
while(true) {
synchronized (locker) {
try {
while (queue.isEmpty()) {
//队列为空, 等待
locker.wait();
}
MyTimerTask task = queue.peek();
//获取当前时间
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
//时间到了, 执行并删除它
task.run();
queue.poll();
} else {
//未到达指定时间
locker.wait(task.getTime() - curTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//不要忘记!!
t.start();
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
MyTimer time = new MyTimer();
time.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1");
}
},3000);
time.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2");
}
},5000);
}
}
到这里就实现完成啦~