前言
本篇了解定时器的方法,参数,并模拟实现定时器,进一步对定时器有了更深的理解,如有错误,请在评论区指正,让我们一起交流,共同进步!
文章目录
本文开始
1. 认识定时器
1.什么是定时器?
定时器就是设置一个时间,等待时间到了,就执行已经写好的代码;
2.定时器的类
Java标准库提供的定时器:Timer 它是java.util提供的一个类;
定时器的核心方法:schedule();
它有两个参数:第一个是实现要执行的任务代码,第二个是多长时间之后执行(毫秒级别);
定时器代码实现:
import java.util.Timer;
import java.util.TimerTask;
public class Test {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("run2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("run1");
}
},1000);
System.out.println("main");
}
}
【注】这里的new TimerTask() 接口实现了Runnable 方法;
2. 模拟实现定时器
2.1 创建MyTimer类
定时器主要的类就是Timer,这里就得自己实现!
实现这个类就需要用到带有优先级的阻塞队列,为什么呢?
定时器内部有很多任务需要管理,多个线程可能都在执行schedule方法,但是它们的执行时间是不同的,就需要让他们按照顺序执行,并保证线程是安全的,这就用到了带有优先级的阻塞队列;
在多线程条件下,为了保证线程是安全的,此时Java标准库中有优先级阻塞队列就满足这些条件;
使用优先级阻塞队列: PriorityBlockingQueue
但是问题又来了,优先级阻塞队列里面放置什么元素呢?
优先级队列中存放的元素是什么?
因为放置的元素不是单一的,需要创建一个类,封装一下①执行的任务是什么(Runnable);②任务什么时候执行(time)
2.2 创建优先级队列中放置的元素MyTask
1.描述执行的任务
public Runnable runnable;
2.描述要执行任务的时间点
public long time;
这里为了方便后续判定,使用绝对的时间戳;
取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳;
3.构造方法为下面初始化参数做准备
构造public MyTask()
2.3 在MyTimer类写核心方法schedule
1.参数
①Runnable runnable - 执行的任务;② long delay - 执行任务要等待的时间;
2.根据参数构造创建对象MyTask, 并把对象插入到阻塞队列中
public void schedule(Runnable runnable, long delay) {
//创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
}
2.4 构造线程,执行具体任务
1.创建无参 public MyTimer() {…}
2.创建线程 t
3.使用循环,每次取出阻塞队列中的元素(任务),获取当前任务的当前时间,判断任务执行时间与当前时间的大小;
public MyTimer() {
Thread t = new Thread( () -> {
//使用循环不断取出阻塞队列中的元素,比较是否到任务执行的时间
while (true) {
try {
//取出阻塞队列中的元素
MyTask myTask = queue.take();
//获取当前任务的时间
long curTime = System.currentTimeMillis();
//比较时间到了就执行任务,时间没有到就放回到阻塞队列中
if(myTask.time < curTime) {
//执行任务
myTask.runnable.run();
}else {
//放回阻塞队列中
queue.put(myTask);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
基本的定时器框架已经完成,但是并不能执行,思考一下,这个定时器存在什么问题呢???
从代码中分析一下吧!定时器不完全代码:
class MyTask {
public Runnable runnable;//描述要执行的任务
public long time;//描述任务要执行的时间 =》 这里采取绝对时间
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
//当前时间 + dealy = 执行任务的时间
this.time = System.currentTimeMillis() + delay;
}
}
class MyTimer {
//构造优先级阻塞队列
PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//构造schedule方法,delay=> 延迟
public void schedule(Runnable runnable, long delay) {
//创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
}
//创建线程
public MyTimer() {
Thread t = new Thread( () -> {
//使用循环不断取出阻塞队列中的元素,比较是否到任务执行的时间
while (true) {
try {
//取出阻塞队列中的元素
MyTask myTask = queue.take();
//获取当前任务的时间
long curTime = System.currentTimeMillis();
//比较时间到了就执行任务,时间没有到就放回到阻塞队列中
if(myTask.time < curTime) {
//执行任务
myTask.runnable.run();
}else {
//放回阻塞队列中
queue.put(myTask);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
来看一下存在的问题!!!
2.5 出现的两种问题
1.优先级队列的比较规则
产生原因:在优先级阻塞队列中,每个任务执行的时间是不同的,为了保证有序执行,使用优先级阻塞队列,但是比较两个任务的执行时间的规则没有定,无法正常排序,所以需要实现自定义的比较规则;
解决方式:实现Comparable方法,重写comparTo()
代码实现:
class MyTask implements Comparable<MyTask>{
public Runnable runnable;//描述要执行的任务
public long time;//描述任务要执行的时间 =》 这里采取绝对时间
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
//取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
//currentTimeMillis()=> 时间戳:当前时刻和基准时刻的ms数之差
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
//获取每次取出最小的时间
return (int)(time - o.time);
}
}
2.忙等
产生原因:假设一个任务执行的时间为12点,当前时间是11点30;在这期间,一直读取当前时间,cup一直处于被暂用的状态,会有资源的浪费 =》这就是忙等
解决方式:当没有到达任务要执行的时间,使用wait()方法等待,等时间到了再执行;
【注】wait() 需要配合加锁一起操作,就需要创建新的锁对象;
代码实现:在MyTimer中创建锁对象,调用wait()方法,这里简化跟上述重复的代码就不写了,只写增加的代码的部分;
//在MyTimer内
//使用wait方法,就需要加锁,加锁就需要锁对象,创建锁对象
private Object locker = new Object();
//创建线程
public MyTimer() {
Thread t = new Thread( () -> {
while (true) {
try {
synchronized (locker) {
//取出阻塞队列中的元素
MyTask myTask = queue.take();
//获取当前任务的时间
long curTime = System.currentTimeMillis();
if(myTask.time < curTime) {
//执行任务
myTask.runnable.run(); //???
}else {
//时间没到,放回阻塞队列中
queue.put(myTask);
//防止忙等,使用wait等待时间的差值
locker.wait(myTask.time - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
加锁wait()后,发现如果出现新的任务怎么办呢?
解决方式:在插入阻塞队列时,唤醒wait(), 可以使用notify()
代码实现:
//构造schedule方法,delay=> 延迟
public void schedule(Runnable runnable, long delay) {
//创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
//在加入新任务的时候,就唤醒
synchronized (locker) {
locker.notify();
}
}
2.6 测试代码
测试思想描述:
创建定时器,多个线程调用schdule方法,规定执行的任务和经过多次时间执行这个任务;
主函数测试代码:
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("run2");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("run1");
}
},1000);
System.out.println("main");
}
执行结果:
3. 定时器总代码
代码如下:
import sun.applet.Main;
import java.util.concurrent.PriorityBlockingQueue;
//创建一个类封装一些,用来表示一个任务
class MyTask implements Comparable<MyTask>{
//描述执行的任务
public Runnable runnable;
//为了方便后续判定,使用绝对时间戳
public long time;//描述这个任务要执行的时间点 =》 任务要执行的绝对时间
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
//取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
//currentTimeMillis()=> 时间戳:当前时刻和基准时刻的ms数之差
this.time = System.currentTimeMillis() + delay;
}
//阻塞队列的比较规则
@Override
public int compareTo(MyTask o) {
//每次取出时间最小的元素
return (int)(time - o.time);
}
}
class MyTimer {
//使用带有优先级的阻塞队列
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//①优先级的比较规则:阻塞队列中MyTask元素的比较,需要设置一个比较规则
//创建锁对象,进行加锁操作
private Object locker = new Object();
//定时器中的schedule方法
//delay: 是一个像2000这样的数字(表示怎么长时间后,执行该任务)
public void schedule(Runnable runnable, long delay) {
//根据参数,构造MyTask, 插入阻塞对列
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
//新的任务来了:插入元素的时候,唤醒等待
synchronized (locker) {
locker.notify();
}
}
//构造线程,负责执行具体的任务
public MyTimer() {
//创建线程
Thread t = new Thread( () -> {
//②忙等:循环true会一直访问(相当于一直看时间到了执行时间没有)这样会一直占用cpu =》这就是忙等
//解决方法:等待过程需要释放cpu,使用wait操作,可以随时提前结束
while (true) {
try {
//了解阻塞队列,只有则是队列的入队列和出队列,没有阻塞队列的查看对首元素
// =》只能出队列后才能看到该任务并判断到执行时间了没有,没有会塞会阻塞队列中
synchronized (locker) {
//取出阻塞队列中的元素
MyTask myTask = queue.take();
//获取当前任务的时间
long curTime = System.currentTimeMillis();
if(myTask.time <= curTime) {
//时间到了,执行任务;=》执行时间小于当前时间
myTask.runnable.run(); //???
}else {
//执行任务的时间还没到,
//把刚才取出的任务,重新塞会阻塞队列中
queue.put(myTask);
locker.wait(myTask.time - curTime);
//等待过程中,新任务来了为什么要中间唤醒原来的等待???
//因为可能会遇到下一个任务的时间比等待的时间要短的任务
// 此时唤醒等待,在取任务元素的时候,就会取到时间短的任务 =》 任务有优先级的排列
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("run3");
}
},3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("run2");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("run1");
}
},1000);
System.out.println("main");
}
}
总结
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!