定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
定时器是一种实际开发中非常常用的组件. 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
}
其中TimerTask()就是一个实现了Runnable的抽象类:
可以把它的作用看成是给定时器一个任务,而第二个参数就是指定多久时间后执行这个任务。
思考一下定时器的构成需要哪些?
1)写一个任务类,任务类还必须能够按照时间来比大小,因为优先级阻塞队列需要比较大小
//任务类 描述任务和任务的delay时间
static class Task implements Comparable{
//任务
private Runnable command;
//delay
private long time;
public Task(Runnable command,long time){
this.command = command;
//时间是在现在的时间的基础上加上delay
this.time = System.currentTimeMillis() + time;
}
public void run(){
command.run();
}
@Override
public int compareTo(Task o) {
return (int)(this.time - o.time);
}
}
2)需要有一个优先级阻塞队列来存放用户注册的任务
//优先级阻塞队列 核心结构
//队首存放的是最近要执行的任务 time最小
private PriorityBlockingQueue queue = new PriorityBlockingQueue();
public void schedule(Runnable command,long time){
//生成一个任务 然后放进去优先级队列
Task task = new Task(command, time);
queue.put(task);
}
3)在构造方法中整一个线程对队首元素扫描,看是否到了执行时间
public MyTimer(){
//在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
Thread work = new Thread(() -> {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
work.start();
}
用写一个测试:
//测试代码
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第一个任务!");
}
},3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第二个任务!");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第三个任务!");
}
},2000);
}
此时已经可以按照定时器的工作原理来完成任务了:
但是当前的代码还存在着比较严重的问题,就是在3)中如果时间没有到的话会存在cpu一直比较的情况。举个例子,比如小明九点上班,他七点在床上突然醒了。正常情况下应该是继续睡睡到平时订的闹钟时间,但是如果小明一直看表一直看表知道闹铃响起,这样既没有休息也没有做有意义的事情,是十分愚蠢的行为。代码的问题也就在于此,如果没有到执行时间,不管还有多久还都会一直比较有没有到执行时间是没有意义的,也就是处于”忙等“状态。
优化的话,应该让系统在看到当前队首任务还没有到达执行时间的时候就执行wait(时间差)。但是此时还存在另外一个问题,系统wait一段时候之后确实会执行队首的任务,但是如果在wait的时间中又来了新的任务并且新的任务重新处于了队首,此时就会出bug了。正确的做法是在每次有新的任务被注册的时候都通知一下结束wait。
修改代码:
1.引入一个lock对象,借助该对象的wait/notify来解决忙等状态
private Object lock = new Object();
2.修改构造方法中的work的工作方法
public MyTimer(){
//在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
Thread work = new Thread(() -> {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
// 等待一段时间
synchronized (lock){
lock.wait(task.time - curTime);
}
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
work.start();
}
3. 修改 Timer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 worker 线程. (因为新插入的任务可能是需要马上执行的).
public void schedule(Runnable command,long time){
//生成一个任务 然后放进去优先级队列
Task task = new Task(command, time);
queue.put(task);
//有新任务来了 唤醒work 检测是否有更新的工作需要执行
synchronized (lock){
lock.notify();
}
}
完整代码:
public class MyTimer {
//任务类 描述任务和任务的delay时间
static class Task implements Comparable{
//任务
private Runnable command;
//delay
private long time;
public Task(Runnable command,long time){
this.command = command;
//时间是在现在的时间的基础上加上delay
this.time = System.currentTimeMillis() + time;
}
public void run(){
command.run();
}
@Override
public int compareTo(Task o) {
return (int)(this.time - o.time);
}
}
//优先级阻塞队列 核心结构
//队首存放的是最近要执行的任务 time最小
private PriorityBlockingQueue queue = new PriorityBlockingQueue();
public void schedule(Runnable command,long time){
//生成一个任务 然后放进去优先级队列
Task task = new Task(command, time);
queue.put(task);
//有新任务来了 唤醒work 检测是否有更新的工作需要执行
synchronized (lock){
lock.notify();
}
}
private Object lock = new Object();
public MyTimer(){
//在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
Thread work = new Thread(() -> {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
// 等待一段时间
synchronized (lock){
lock.wait(task.time - curTime);
}
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
work.start();
}
//测试代码
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第一个任务!");
}
},3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第二个任务!");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我有第三个任务!");
}
},2000);
}
}
此时代码还有问题吗????
理论上说说代码中还是有一点小问题的。(烧脑啊.....)上图:
了解了上述问题之后,就不难发现,问题出现的原因,是因为当前 take 操作,和 wait 操作,并非是原子的如果在 take 和 wait 之间加上锁,保证在这个过程中,不会有新的任务过来,问题自然解决(换句话说只要保证每次 notify 时确实都正在 wait )