定时器: Timer
- 一、定时器是什么
- 二、标准库中的定时器
- 三、实现定时器
- 3.1 定时器的构成
- 3.2 实现细节
- 3.3 完整代码
一、定时器是什么
定时器也是软件开发中的一个重要组件。类似于一个 “闹钟”,即达到一个设定的时间之后,就执行某个指定好的代码。
定时器是一种实际开发中非常常用的组件。
比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连!
比如一个 Map,希望里面的某个 key 在 3s 之后过期 (自动删除)!
类似于这样的场景就需要用到定时器~~
二、标准库中的定时器
- 标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule。
- 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("时间到, 快起床!");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到2!");
}
}, 4000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到3!");
}
}, 5000);
System.out.println("开始计时!");
}
}
执行完上述任务之后,进程并没有退出!
因为Timer内部需要一组线程来执行注册的任务,而这里的线程是前台线程,会影响进程退出~~
三、实现定时器
3.1 定时器的构成
- 一个带优先级的阻塞队列;
为啥要带优先级呢?
因为阻塞队列中的任务都有各自的执行时刻 (delay),最先执行的任务一定是 delay 最小的,使用带优先级的队列就可以高效地把这个 delay 最小的任务找出来~~
- 队列中的每个元素是一个 Task 对象;
- Task 中带有一个时间属性,队首元素就是时间最小的 Task;
- 同时有一个 worker 线程一直扫描队首元素,看队首元素是否需要执行!
3.2 实现细节
1)schedule第一个参数是一个任务,包含两个信息:一个是要执行啥工作;一个是啥时候执行
// 这个类表示一个任务class MyTask
// 要执行的任务
private Runnable runnable;
// 什么时间来执行任务(是一个时间戳)
private long time;
public MyTask(Runnable runnable,long delay) {
this.runnable = runnable;
this.time = System.currentTimeMiLlis() + delay;
}
2)让 MyTimer 能够管理多个任务 (一个Timer是可以安排多个任务的)
前面提及:一个带优先级的阻塞队列最合适!
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
3)任务已经被安排到优先级阻塞队列中了,接下来就需要从队列中取元素了。
创建一个单独的扫描线程,让这个线程不停的来检查队首元素,看时间是否到了。如果时间到了,则执行该任务!
4)优先级阻塞队列需要进行元素比较,所以要实现Comparable接口!
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
5)还存在严重的问题!!!
可以使用wait来阻塞线程,当schedule加入新任务时再唤醒!:
Thread t = new Thread(() -> {
while (true){
try {
//取出队首元素
MyTask task = queue.take();
//假设当前时间是2:30,任务设定的时间是2:30,显然就要执行任务了
//假设当前时间是2:30,任务设定的时间是2:29,也是到点了,也要执行任务long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()){
//到点了,改执行任务了!!
task.getRunnable(). run();
} else {
//还没到点
queue.put(task);
//没到点,就等待
synchronized (locker){
locker.wait( timeout: task.getTime() - curTime);
}
}
}catch (InterruptedException e) {
e.printstackTrace();
}
}
});
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
这时的代码还有一个问题!!!:
假设扫描线程先执行,执行take之后线程切换到schedule线程。
schedule线程新增一个任务,这个任务1:00 执行。schedule执行完毕之后,执行notify ( t线程刚执行完take,还没wait呢~ 这个notify相当于空打了一炮:虽然通知了,但是没有唤醒任何线程),然后回到扫描线程继续往下执行,然后发现当前时刻是12:00,任务时间是2:30?!这时就把任务塞回队列,然后就进行wait,wait时间是2.5小时!
这就意味着,刚才新来的这个1:00要执行的任务,就被错过了!!!
多线程的执行过程是非常复杂的,任何两行代码之间都可能出现线程切换,甚至一行代码中就可能会切换多次~~
写代码的时候脑子里就得演绎出各种各样的情况!!!
因此需要把锁的范围放大:
public MyTimer() {
// 创建一个扫描线程.
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
// 取出队首元素
MyTask task = queue.take();
// 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
// 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 到点了, 改执行任务了!!
task.getRunnable().run();
} else {
// 还没到点
queue.put(task);
// 没到点, 就等待
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
刚才出现问题的原因就是notify在take和wait之间执行的。
现在把扫描线程中的锁范围放大了,此时就可以避免notify在take和wait之间执行了!扫描线程会先拿到锁,然后take,然后中间逻辑,一直到wait;在这个过程中,schedule线程会阻塞等待锁。直到扫描线程执行了wait后,扫描线程释放了锁,schedule线程就拿到了锁,进行了通知,这个时候wait就被立即唤醒了!接下来再次重新取队首元素,就把1:00执行的任务取出来了~~
如果把schedule方法里的锁范围也扩大可以吗?:
public void schedule(Runnable runnable, long after) throws InterruptedException {
synchronized (locker) {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
locker.notify();
}
}
运行代码后,我们发现两个线程都会进入阻塞状态,即死锁!!!为什么呢?
如果代码死锁了,一定要先拿jconsole看下线程的调用栈,明确死锁是卡死在哪行代码!
通过jconsole,我们找到死锁位置:
原因:假设此处是先执行42行这里的代码:先加锁,然后尝试从队列里take取队首元素。而queue是一个阻塞队列,特点就是队列为空时取值则阻塞!!!
此时扫描线程就阻塞在45行了,什么时候会解除阻塞?得有线程往队列里加元素!
主线程69行要通过schedule往里加元素,但是加元素的前提是先加锁,但是此时这个锁是被43行代码扫描线程占用着呢,schedule获取不到锁,无法执行put!!!
这不就死锁了吗?!~~
阻塞队列take操作wait的时候是释放队列内部的锁对象,这个代码中还有一个自己定义的locker对象。
两个线程两把锁~~
3.3 完整代码
import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
// 这个类表示一个任务
class MyTask implements Comparable<MyTask> {
// 要执行的任务
private Runnable runnable;
// 什么时间来执行任务. (是一个时间戳)
private long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
// 创建一个扫描线程.
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
// 取出队首元素
MyTask task = queue.take();
// 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
// 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 到点了, 改执行任务了!!
task.getRunnable().run();
} else {
// 还没到点
queue.put(task);
// 没到点, 就等待
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到1!");
}
}, 3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到2!");
}
}, 4000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到3!");
}
}, 5000);
System.out.println("开始计时");
ArrayDeque<String> a = new ArrayDeque<>();
a.peekLast();
}
}