🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔🦾🦾🦾
目录
3.注意,如果我们的锁只锁wait这一行代码会发生一种极端情况
前言
🐳🐳定时器相当于一个任务管理器。有些任务可能现在执行, 有些任务可能过1个小时,甚至很久才会执行。定时器就是对这些任务进行管理监视, 如果一个任务执行时间到了,定时器就会将这个任务执行。 保证所有的任务都会在合适的时间执行。
一.定时器(Timer)的使用
1.自定义一个类继承于TimerTask
的类,并重写其run()
方法即可。
2.可以采取匿名类的形式,直接重写其run()
方法。
二.定时器的方法
TimeTask有一抽象方法run()
,其作用就是用来放我们处理的逻辑任务。
Timer有一schedule()
方法,重载参数和另外两个方法如下表:
代码演示:
public class ThreadDemo3 {
public static void main(String[] args) {
//定时器
//Timer内置了线程(前台线程),会阻止进程结束
Timer timer =new Timer();
//TimerTask实现了Runnable接口,对Runnable进行了封装,本质上还是Runnable
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("玄武一号任务执行, 执行代号:闪电; 定时时间:3s");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("朱雀二号任务执行, 执行代号:暴风; 定时时间:5s");
}
},5000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("青龙三号任务执行, 执行代号:狂风; 定时时间:7s");
}
},7000);
timer.schedule(new TimerTask() {
@Override
public void run() { //这个方法的执行是靠Timer内部的线程在时间到了之后执行的
System.out.println("白虎四号任务执行, 执行代号:地震; 定时时间:10s");
}
},10000);
System.out.println("主线程任务执行 未定时"); //主线程main直接执行
}
}
三.自定义一个定时器
我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:
1.一个扫描线程,负责来判断任务是否到时间需要执行
2.需要有一个数据结构来保存我们定时器中提交的任务
创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个MyTask对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityBlockingQueue比较合适。
1.首先构建一个MyTask对象,表示一个任务
//表示一个任务
class MyTask implements Comparable<MyTask> {
//这个是我们要执行任务
public Runnable runnable;
//为了方便判定,使用了绝对的时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable =runnable;
//取当前时刻的时间戳+delay,作为该任务实际执行的时间戳
this.time =System.currentTimeMillis()+delay; //实例化这个参数,,这个time就是系统时间加上延迟延迟时间
}
@Override
public int compareTo(MyTask o) {
//这样的写法表示每次取出的是最小元素
return (int)(this.time - o.time); //解决优先级阻塞队列的优先级问题
}
}
这个类实现了Comparable接口并且重写compareTo方法, 指明我们是根据时间来决定这个任务在队列中的优先级.
2.实现一个定时器(内置一个扫描线程)
扫描线程t中包含优先级阻塞队列(小根堆)PriorityBlockingQueue和 循环监管的流程。
MyTimer对象封装了扫描线程线程t 和 任务的添加方法schedule()
关于扫描线程的优化
2.1 循环监控存在一个弊端,那就是一直循环判断, 占用CPU资源。
(假如堆首任务的执行是1小时后, 再次期间监管线程会跑1小时循环判断。)
解决方法: 可以通过线程阻塞和唤醒来解决。在下面代码有详细注释和实现。
2.2 如果任务1小时后执行, 我们让扫描线程wait(1小时)并释放锁进行阻塞等待, 但在此期间如果有新的任务添加进来(可能新的任务需要等30分钟就可以执行,堆首元素发生变化) 就会通过notify唤醒wait,这时需要唤醒扫描线程来重新判断新的等待时间。
class MyTimer {
//创建一个锁对象
private Object locker =new Object();
//这个结构,带有优先级的阻塞队列,核心数据结构
private PriorityBlockingQueue<MyTask> queue =new PriorityBlockingQueue<>();
//此处的delay形如 3000 这样的数字(多长时间执行该任务)
public void schedule(Runnable runnable,long delay) {
//这个方法的作用就是用来实例化MyTask里面的参数的,然后把这个任务插入到优先级阻塞队列中
MyTask myTask =new MyTask(runnable,delay);
queue.put(myTask);
//每次添加任务用notify唤醒wait重新扫描任务
synchronized (locker) {
locker.notify();
}
}
//在这里构造扫描任务的线程,时间到了负责执行具体任务了
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等待解决忙等问题,避免cpu浪费资源,同时释放掉锁
locker.wait(myTask.time - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
3.注意,如果我们的锁只锁wait这一行代码会发生一种极端情况
假设我们的扫描线程刚执行完put方法,这个线程就被cpu调度走了,此时我们的另一个线程调用了schedule,添加了新任务,新任务是12:10执行,然后notify,因为我们并没有wait(),所以相当于这里是notify并没有发挥唤醒作用,然后我们的线程调度回来去执行wait()方法,但是我们的时间差仍然是之前算好的时间差,从14:00点到14:30点,这样会导致新加入的任务不能在我预期的时间执行!!!
这里造成这样的问题,是因为我们的take操作和wait操作不是原子的,我们需要在take和wait之间加上锁,保证每次notify的时候,都在wait中。
4.测试代码
public class ThreadDemo4 {
public static void main(String[] args) {
MyTimer myTimer =new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("玄武一号任务执行, 执行代号:闪电; 定时时间:10s");
}
},10000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("朱雀二号任务执行, 执行代号:暴风; 定时时间:7s");
}
},7000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("青龙三号任务执行, 执行代号:狂风; 定时时间:5s");
}
},5000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("白虎四号任务执行, 执行代号:地震; 定时时间:3s");
}
},3000);
System.out.println("主线程任务执行 未定时"); //主线程main直接执行
}
需要注意理解的是:这段代码一共有两个线程在同时执行,main线程就是那个添加任务的线程,t线程一直就做一件事,就是在循环里一直扫描判断是否需要执行任务