此篇文章与大家分享多线程的第七篇文章——关于定时器
如果有不足的或者错误的请您指出!
4.定时器
所谓定时器就是类似于闹钟效果,指定一个任务给他,这个任务不会立即执行.而是到达指定的时间后才执行
定时器在实际开发中非常重要,甚至会单独封装成一个服务器,给整个分布式系统使用
4.1标准库提供的定时器
这里的TimeTask实际上是继承了Runnable接口的
同时,Timer内部包含的也是前台线程,阻止了进程结束
4.2自己实现一个定时器
需求:能够延迟执行任务 ,能够管理多个任务
需要有:定义一个类.表示一个任务
通过一定的数据结构来保存多个任务
还需要有一个线程,来负责执行这里的任务(在指定之间内去执行)
4.2.1任务类
public class MyTimeTask {
/*
当前自己定义的任务里面要保存执行任务的绝对的时间(时间戳)
为了后续线程执行的时候,可以方便的判定,该任务是否执行
*/
private long time;
private Runnable runnable;
public MyTimeTask(Runnable runnable ,long delay) {
this.time = System.currentTimeMillis() + delay;//手动换算时间
this.runnable = runnable;
}
}
4.2.2Timer类
public class MyTimer {
/*
这里实际上最直观的是使用List
但是如果这里元素很多,就需要不停循环的扫描这里的任务,分别判定是否要执行
那么我们就可以将这些任务通过优先级队列保存起来,按照时间作为优先级队列的先后标准,就可以做到,队首元素就是最先要执行的任务,那么线程扫描的时候直接判断队首元素即可
那么我们就要对任务类实现comparable接口
*/
PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
public void schedule(Runnable runnable,int delay) {
MyTimeTask timeTask = new MyTimeTask(runnable,delay);
queue.offer(timeTask);
}
}
此时别忘了对MyTimeTask类实现Comparable接口
public class MyTimeTask implements Comparable<MyTimeTask>{
/*
当前自己定义的任务里面要保存执行任务的绝对的时间(时间戳)
为了后续线程执行的时候,可以方便的判定,该任务是否执行
*/
private long time;
private Runnable runnable;
public MyTimeTask(Runnable runnable ,long delay) {
this.time = System.currentTimeMillis() + delay;//手动换算时间
this.runnable = runnable;
}
@Override
public int compareTo(MyTimeTask o) {
return (int)(this.time - o.time);
}
}
4.2.3 有一个线程来负责执行这里的任务
public class MyTimer {
/*
这里实际上最直观的是使用List
但是如果这里元素很多,就需要不停循环的扫描这里的任务,分别判定是否要执行
那么我们就可以将这些任务通过优先级队列保存起来,按照时间作为优先级队列的先后标准,就可以做到,队首元素就是最先要执行的任务,那么线程扫描的时候直接判断队首元素即可
那么我们就要对任务类实现comparable接口
*/
PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
public MyTimer () {
Thread t = new Thread (() -> {
while(true) {
//此时就要判断当前优先级任务队列的头任务是否到达执行时间了
//如果到达则执行,不到达则循环判断
}
});
}
public void schedule(Runnable runnable,int delay) {
MyTimeTask timeTask = new MyTimeTask(runnable,delay);
queue.offer(timeTask);
}
}
public class MyTimeTask implements Comparable<MyTimeTask>{
/*
当前自己定义的任务里面要保存执行任务的绝对的时间(时间戳)
为了后续线程执行的时候,可以方便的判定,该任务是否执行
*/
private long time;
private Runnable runnable;
public MyTimeTask(Runnable runnable ,long delay) {
this.time = System.currentTimeMillis() + delay;//手动换算时间
this.runnable = runnable;
}
public void run() {
this.runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimeTask o) {
return (int)(this.time - o.time);
}
}
那么我们就来具体实现schedule内的细节
public MyTimer () {
Thread t = new Thread (() -> {
while(true) {
//此时就要判断当前优先级任务队列的头任务是否到达执行时间了
//如果到达则执行,不到达则循环判断
if(queue.isEmpty()) {
continue;
}
long curTime = System.currentTimeMillis();
MyTimeTask task = queue.peek();
if(task.getTime() <= curTime) {
task.run();
queue.poll();
}else{
//时间未到
continue;
}
}
});
}
但是此时可能存在不同线程同时修改同一个队列的情况,就要加入锁
public class MyTimer {
/*
这里实际上最直观的是使用List
但是如果这里元素很多,就需要不停循环的扫描这里的任务,分别判定是否要执行
那么我们就可以将这些任务通过优先级队列保存起来,按照时间作为优先级队列的先后标准,就可以做到,队首元素就是最先要执行的任务,那么线程扫描的时候直接判断队首元素即可
那么我们就要对任务类实现comparable接口
*/
PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public MyTimer () {
Thread t = new Thread (() -> {
synchronized (locker) {
while(true) {
//此时就要判断当前优先级任务队列的头任务是否到达执行时间了
//如果到达则执行,不到达则循环判断
if(queue.isEmpty()) {
continue;
}
long curTime = System.currentTimeMillis();
MyTimeTask task = queue.peek();
if(task.getTime() <= curTime) {
task.run();
queue.poll();
}else{
//时间未到
continue;
}
}
}
});
}
public void schedule(Runnable runnable,int delay) {
synchronized (locker) {
MyTimeTask timeTask = new MyTimeTask(runnable,delay);
queue.offer(timeTask);
}
}
}
此时还存在两个比较核心的问题:
(1)上述的循环等待,实际上这个代码逻辑处于"忙等"的状态,确实是在等,但是等的过程中很忙,比如14:00要执行任务,但是13:00就开始等了
上述代码在短时间内疚会循环很多次,上述操作都是在"空转",一直在消耗cpu,没有真正执行任务
而我们要实现的就是在等待的时间内,要释放cpu资源
public class MyTimer {
/*
这里实际上最直观的是使用List
但是如果这里元素很多,就需要不停循环的扫描这里的任务,分别判定是否要执行
那么我们就可以将这些任务通过优先级队列保存起来,按照时间作为优先级队列的先后标准,就可以做到,队首元素就是最先要执行的任务,那么线程扫描的时候直接判断队首元素即可
那么我们就要对任务类实现comparable接口
*/
PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public MyTimer () {
Thread t = new Thread (() -> {
try {
while(true) {
synchronized (locker) {
//此时就要判断当前优先级任务队列的头任务是否到达执行时间了
//如果到达则执行,不到达则循环判断
if(queue.isEmpty()) {
locker.wait();//队列为空,等待,直到有新的任务进来
}
long curTime = System.currentTimeMillis();
MyTimeTask task = queue.peek();
if(task.getTime() <= curTime) {
task.run();
queue.poll();
}else{
locker.wait(task.getTime() - curTime);
//时间未到,等待,直到有新的任务进来(判断新的任务是否要执行)
//或者时间到了,执行
}
}
}
}catch(InterruptedException e) {
e.printStackTrace();
}
});
}
public void schedule(Runnable runnable,int delay) {
synchronized (locker) {
MyTimeTask timeTask = new MyTimeTask(runnable,delay);
queue.offer(timeTask);
locker.notify();
}
}
}
为什么这里不使用PriorityBlockingQueue呢??
实际上不如手动加锁,因为引入阻塞队列只能解决队列为空的阻塞,而时间没到的阻塞还是要我们自己去实现,还但是要引入新的锁,代码就搞复杂了,并且阻塞队列里面本来就有一把锁,这样反而可能导致死锁的出现
这里的wait能不能换成sleep?? ----不行!!!
notift唤醒wait,属于常规手段,是我们处理正常业务的流程,但是sleep通过interrupt唤醒,是处理异常业务的
此外,更加致命的是,wait休眠期间会释放锁,但是sleep可不会释放锁