JavaEE之定时器及自我实现
在生活当中,有很多事情,我们不是立马就去做,而是在规定了时间之后,在到该时间时,再去执行,比如:闹钟、定时关机等等,在程序的世界中,有些代码也不是立刻执行,那么我们该如何实现呢?一探究竟——>《定时器》
1. 定时器
定时器是什么
定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执行某个指定好的代码.
定时器是⼀种实际开发中非常常用的组件.
比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连.
比如⼀个Map,希望里面的某个key在3s之后过期(自动删除).
类似于这样的场景就需要用到定时器.
在Java当中也给我们提供了定时器(Timer)的类,请见下文。
标准库中的定时器
- 标准库中提供了一个Timer类.Timer类的核心方法为
schedule
schedule
包含两个参数.第⼀个参数指定即将要执行的任务代码,第⼆个参数指定多长时间之后执行(单位为毫秒).
2. 自我实现一个定时器
1.首先定时器是用于处理任务的,我们该如何在定时器当中管理任务呢??
我们通过一个类,描述任务和任务执行的时间
具体任务的逻辑用Runnble表示,执行时间的可以用一个long型delay去表示
/**
* 任务类
*/
//由于需要比较时间大小,所以使用接口
class MyTask implements Comparable<MyTask>{
//任务
private Runnable runnable = null;
//延迟时间
private long time = 0;
public MyTask(Runnable runnable, long delay) {
//任务不能为空
if(runnable==null){
throw new RuntimeException("任务不能为空...");
}
//时间不能为负数
if(delay<0){
throw new RuntimeException("时间不能为负数...");
}
this.runnable = runnable;
// 计算出任务执行的具体时间
this.time = delay+System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
//比较当前任务和其他任务的时间
@Override
public int compareTo(MyTask o) {
return (int) (o.getTime()-this.getTime());
}
}
2.通过MyTask描述了任务之后,由于任务的执行顺序不一样,我们该如何去管理任务呢?
我们通过一个优先级队列把任务的对象组织起来
//用阻塞队列来管理任务
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
3.我们描述完了任务也通过优先级队列管理了任务对象,我们如何让任务对象和定时器关联起来呢?
/**
* 添加任务的方法
* @param runnable 任务
* @param delay 延时
* @throws InterruptedException
*/
public void schedule(Runnable runnable, long delay) throws InterruptedException {
// 根据传处的参数,构造一个MyTask
MyTask task=new MyTask(runnable,delay);
// 把任务放入阻塞队列
queue.put(task);
}
}
4.我们通过schedule方法把任务对象添加到了阻塞队列当中,我们只需要创建一个线程来执行任务即可
此时我们的思路是:创建一个线程不停的扫描任务,取出队列的首元素若时间到就取出执行,时间没到就放回队列不执行,就能写出以下代码:
// 创建扫描线程
Thread thread=new Thread(()->{
//不断的扫描队列中的任务
while (true){
try {
//1.从阻塞队列中获取任务
MyTask task = queue.take();
//2.判断到没到执行时间
long currentTime=System.currentTimeMillis();
if(currentTime>=task.getTime()){
//时间到了就执行任务
task.getRunnable().run();
}else {
// 没有到时间,重新放回队列
queue.put(task);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}},"scanThread");
//启动线程
thread.start();
但是上面的代码有一个很明显的问题,就是 “忙等” ,为什么呢?
那么我们怎么解决这个忙等这个问题呢?
在放回队列时让程序等待一段时间等待一段时间
时间为:下一个任务的执行时间和当前时间的差
那么既然要等待了我们必须要通过持有同一个锁,来完成等待操作,所以我们创建一把锁
修改代码如下:
// 创建扫描线程
Thread thread=new Thread(()->{
//不断的扫描队列中的任务
while (true){
try {
//1.从阻塞队列中获取任务
MyTask task = queue.take();
//2.判断到没到执行任务的时间
long currentTime=System.currentTimeMillis();
if(currentTime>=task.getTime()){
//时间到了就执行任务
task.getRunnable().run();
}else {
// 当前时间与任务执行时间的差
long waitTime = task.getTime() - currentTime;
// 没有到时间,重新放回队列
queue.put(task);
synchronized (locker){
//等时间
locker.wait(waitTime);
}
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}},"scanThread");
//启动线程,真正去系统中申请资源
thread.start();
通过锁,解决了忙等问题,
5.此时还有一个新的问题,在该队列中若产生了新的任务执行时间在等待任务之前该怎么办呢?
我们在每一次向阻塞队列当中添加新任务时,我们就唤醒一次扫描线程即可
/**
* 添加任务的方法
* @param runnable 任务
* @param delay 延时
* @throws InterruptedException
*/
public void schedule(Runnable runnable, long delay) throws InterruptedException {
// 根据传处的参数,构造一个MyTask
MyTask task=new MyTask(runnable,delay);
// 把任务放入阻塞队列
queue.put(task);
//在每次添加新任务时,唤醒一次扫描线程,以访扫描线程还在等待,新任务时间过了的问题
synchronized (locker){
locker.notifyAll();
}
}
}
6.CPU调度的过程中可能会产生执行顺序的问题,或当一个线程执行到一半的时间被调度走的现象,会引发什么问题呢?
造成该现象的原因是没有保证原子性,我们扩大锁范围即可解决该问题,修改后的代码如下:
//不断的扫描队列中的任务
while (true){
synchronized (locker){
try {
//1.从阻塞队列中获取任务
MyTask task = queue.take();
//2.判断到没到执行任务的时间
long currentTime=System.currentTimeMillis();
if(currentTime>=task.getTime()){
//时间到了就执行任务
task.getRunnable().run();
}else {
// 当前时间与任务执行时间的差
long waitTime = task.getTime() - currentTime;
// 没有到时间,重新放回队列
queue.put(task);
locker.wait(waitTime);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.由于进入锁之后,MyTask task = queue.take();操作,当阻塞队列中没有元素时,就会阻塞等待,直到队列中有可用元素才继续执行,但是由于MyTask task = queue.take();操作持有了锁,导致无法释放锁,添加任务的方法又迟迟取不到锁,导致一个在等着任务执行,一个在等着获取锁添加任务,造成了
“死锁”
现象,我们该如何解决呢?
我们发现在为了解决原子性问题时,我们扩大加锁的范围,却又引入了更大的问题
一般我们两害相全取其轻
为了解决无法及时执行任务的问题,我们创建了一个后台的扫描线程,只做定时唤醒操作,定时1s或者任意时间唤醒执行一次
完整的定时器实现代码如下:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 自我实现定时器
*/
public class MyTimer {
//用阻塞队列来管理任务
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//创建⼀个锁对象
private Object locker = new Object();
public MyTimer() throws InterruptedException {
// 创建扫描线程
Thread thread=new Thread(()->{
//不断的扫描队列中的任务
while (true){
synchronized (locker){
try {
//1.从阻塞队列中获取任务
MyTask task = queue.take();
//2.判断到没到执行任务的时间
long currentTime=System.currentTimeMillis();
if(currentTime>=task.getTime()){
//时间到了就执行任务
task.getRunnable().run();
}else {
// 当前时间与任务执行时间的差
long waitTime = task.getTime() - currentTime;
// 没有到时间,重新放回队列
queue.put(task);
locker.wait(waitTime);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"scanThread");
//启动线程,真正去系统中申请资源
thread.start();
//创建一个后台线程
Thread daemonThread= new Thread(()->{
while (true){
//定时唤醒
synchronized (locker){
locker.notifyAll();
}
//休眠一会
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//设置成后台线程
daemonThread.setDaemon(true);
//启动线程
daemonThread.start();
}
/**
* 添加任务的方法
* @param runnable 任务
* @param delay 延时
* @throws InterruptedException
*/
public void schedule(Runnable runnable, long delay) throws InterruptedException {
// 根据传处的参数,构造一个MyTask
MyTask task=new MyTask(runnable,delay);
// 把任务放入阻塞队列
queue.put(task);
synchronized (locker){
locker.notifyAll();
}
}
}
/**
* 任务类
*/
class MyTask implements Comparable<MyTask>{
//任务
private Runnable runnable = null;
//延迟时间
private long time = 0;
public MyTask(Runnable runnable, long delay) {
//任务不能为空
if(runnable==null){
throw new RuntimeException("任务不能为空...");
}
//时间不能为负数
if(delay<0){
throw new RuntimeException("时间不能为负数...");
}
this.runnable = runnable;
// 计算出任务执行的具体时间
this.time = delay+System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (o.getTime()-this.getTime());
}
}