前言:
🌈上期博客: 【JavaEE初阶】深入理解多线程阻塞队列的原理,如何实现生产者-消费者模型,以及服务器崩掉原因!!!-CSDN博客
🔥感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客
⭐️小编会在后端开发的学习中不断更新~~~
🥳非常感谢你的支持
目录
[📚️ 1.内容简介](#📚️ 1.内容简介)
📚️ 1.内容简介
🌈Hello!!!uu们,本期小编主要是讲解Java标准库中的一个重要的东西即定时器;
1.定时器在Java标准库中使用方法调用;
2.如何自己在idea上直接手搓实现一个定时器的功能模拟;
📚️2.定时器的使用
2.1使用场景
定时器就是日常生活中常用的组件~~类似于闹钟一样,即设定一个时间,当时间一到那么就会自动执行所规定的任务;
例如:咱们博客上的定时发布文章一样;
即在我们发布博客的时候,存在一个定时发布的选项,这就是定时器在我们之间存在的地方,当然还有我们日常生活中的智能家居,定时完成某个任务~~~,小编就不再过多举例了;
2.2标准库的方法
在Java标准库中,提供了定时器的方法,这里我们就要实施化定时器的对象;
代码如下:
java
Timer timer=new Timer();
然后调用schedule方法,重写run方法,规定我们要执行的任务,以及执行任务的时间;
代码如下:
java
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("此时时间为3秒后");
}
},3000);
注意:这里的3000单位是ms级别的,表示3秒,即3秒后再次执行这个任务;
2.3实现运行
这里小编设置了两个任务,以及两个任务执行的时间,看看最后的运行结果;
代码如下:
java
public class testDemo23 {
public static void main(String[] args) throws InterruptedException {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("此时时间为3秒后");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("此时时间为2秒后");
}
},2000);
此时运行后的演示结果如下图所示:
此时我们可以看到任务的执行依据后面的时间来进行的;
注意:此时进行没有结束,说明这个Timer是一个前台线程(小编上期有讲),这里就是timer不知道是否还有其他的任务,时刻准备着~~~
当然我们是可以一手动来将这个线程结束掉;
代码如下:
java
Thread.sleep(3000);
timer.cancel();
注意:这里的休眠是为了保证任务能够执行完,cancel是为了将这个线程转化为后台线程,main函数执行完后,这个线程也跟着结束;
📚️3.定时器的模拟(重点)
3.1使用的数据结构
我们要明白这个定时器的模拟过程:
1.实现一个线程,负责进行扫描任务,并且还要进行"掐"时间这个过程,当时间一到就运行
2.实现一个数据结构,存储schedule进来的方法
这里线程就要进行这个数据结构的扫描,若时间到了就执行,但是这里的时间复杂度为log(N) 所以这里我们就要使用优先级队列了;
原因:我们可以设置一个比较方法来比较时间,由于先执行的一定是时间最少的,那么时间少的就为队首元素,这里我们就可以直接取队顶元素,这时候我们的时间复杂度为log(1);
3.2任务类
1.属性
代码如下:
java
class MyTimerTask {
// 在什么时间点来执行这个任务.
// 此处约定这个 time 是一个 ms 级别的时间戳.
private long time;
// 实际任务要执行的代码.
private Runnable runnable;
注意:这里我们是持有一个runnable类,也可以实现继承,这里的time是一个毫秒级别的时间戳
2.实现runnable类中的run方法
如下代码:
java
public void run() {
runnable.run();
}
注意:这里使用run方法,就是为了在主函数上执行对应的任务;
3.构造方法,在时间扫描中传递参数
代码如下:
java
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 计算一下真正要执行任务的绝对时间. (使用绝对时间, 方便判定任务是否到达时间的)
this.time = System.currentTimeMillis() + delay;
}
注意:这里的delay是一个相对的时间,而在后面比较是绝对的时间
4.实现时间比较器
代码如下:
java
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
注意:由于利用的是优先级队列,所以这里要规定一个比较的条件,注意还要实现comparable接口
3.3时间扫描类
1.属性
代码入下:
java
class MyTimer {
// 负责扫描任务队列, 执行任务的线程.
private Thread t = null;
// 任务队列
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
// 搞个锁对象, 此处使用 this 也可以.
private Object locker = new Object();
注意:这里的线程是为了保证以下线程启动,即当使用这个类时,线程就要启动,这里还有一个优先级队列的声明,和防止线程安全问题设置的一个锁对象
2.实现任务存储
代码如下:
java
public void schedule(Runnable runnable, long delay) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
}
注意:这里就是将执行的任务传给mytimertask然后保存到优先级队列中
3.构造方法,实现线程自动启动
代码如下:
java
// 构造方法. 创建扫描线程, 让扫描线程来完成判定和执行.
public MyTimer() {
t = new Thread(() -> {
while (true) {
while (queue.isEmpty()) {
// 暂时先不处理
}
MyTimerTask task = queue.peek();
// 获取到当前时间
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 当前时间已经达到了任务时间, 就可以执行任务了.
queue.poll();
task.run();
} else {
// 当前时间还没到, 暂时先不执行
}
}
});
t.start();
}
注意:这里线程要不断进行扫描队列,如果时间到了就执行,没有到就先不做处理,任务执行了就删除,这里要进行时间的比较,若此时时间大于或等于了规定时间,那么就执行
3.4主函数
实现规定任务,时间,代码如下:
java
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000);
}
这里的任务就是run方法里的打印,以及3000所规定的时间要求;
3.5线程安全问题
这里由于是两个线程,一个扫描,一个添加任务线程,那么此时我们可以发现,在线程执行的过程中存在读和写的操作,此时就要进行加锁;
java
while (true) {
synchronized (locker) {
while (queue.isEmpty()) {
// 暂时先不处理
}
注意:这里就要进行加锁打包,小编后面就省略了,当然这里任务添加方法也是需要进行加锁操作的;
3.6实现线程阻塞和唤醒
1.优先级队列为空
**问题:**我们可以发现当队列为空的时候,现场因该进入阻塞的状态,否则直接执行以下代码释放锁之后,会直接导致释放锁又拿到锁,导致线程饿死
所以这里在队列为空的时候我们需要进行线程阻塞,然后再适合的时间进行唤醒:
代码如下:
java
while (queue.isEmpty()) {
// 暂时先不处理
locker.wait();
}
此时唤醒就应该在添加任务之后,所以我们要在添加任务时进行线程的唤醒
代码如下:
java
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
// 添加新的元素之后, 就可以唤醒扫描线程的 wait 了.
locker.notify();
}
}
那么此时添加任务后,线程唤醒,就可以进行执行了;
2.时间未到
**问题:**当时间没有到的时候我们应该进行等待,并且这里的等待是有时间要求的,即超时等待;
代码如下:
java
if (curTime >= task.getTime()) {
// 当前时间已经达到了任务时间, 就可以执行任务了.
queue.poll();
task.run();
} else {
locker.wait(task.getTime() - curTime);
}
注意:进行等待的时间就是这个任务所执行的时间要求,若在此过程中添加了其他更早执行的任务,那么这里的任务添加类就能够进行唤醒的操作;
这里不能使用sleep,因为当来新的任务后,线程不能唤醒解锁,导致错过新的任务,如果是continue的话就会循环执行任务那么此时就叫"忙等"
3.7run方法如何执行任务
小编先将代码执行顺序归为一整个,给小伙伴们讲解一下:
java
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("这是一个3秒后的任务");
}
}, 3000);
public void schedule(Runnable runnable, long delay) {
MytimerTask task = new MytimerTask(runnable, delay);
}
public MytimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
}
public void run() {
runnable.run();
}
task.run();
1.首先我们在主函数规定runnable对象重写run方法的执行任务后,传给schedule
2.在mytimer类中的schedule方法接收参数后给mytimertask类的参数
3.在mytimertask类中接收到传递的参数后,在调用run方法,此时的run方法就是在主函数实现的重写任务方法
4.所以我们只需要在线程执行中通过mytimertask的对象,调用这个方法就好了;
如下图所示:
📚️4.总结
💬💬小编本期主要讲解了关于定时器在Java标准库中的使用方法,以及自主实现了关于定时器的代码模拟,当然这部分是有一定的难度的,这里涉及到"优先级队列,函数的调用,runnable类的使用,以及比较器的设定,线程安全问题,和唤醒阻塞"相关的知识体系,需要各位uu学习了解;
🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!
💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。
😊😊 期待你的关注~~~