多线程 (八) 定时器Timer的使用及实现
🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!
人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!欢迎志同道合的朋友一起加油喔🦾🦾🦾
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个🐒嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心
目录
前言
一.定时器(Timer)的使用
二.定时器的方法
三.自定义一个定时器
1.首先构建一个MyTask对象,表示一个任务
2.实现一个定时器(内置一个扫描线程)
3.注意,如果我们的锁只锁wait这一行代码会发生一种极端情况
4.测试代码
前言
🐳🐳定时器相当于一个任务管理器。有些任务可能现在执行, 有些任务可能过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进行了封装,本质上还是Runnabletimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("玄武一号任务执行, 执行代号:闪电; 定时时间:3s");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("朱雀二号任务执行, 执行代号:暴风; 定时时间:5s");}},5000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("青龙三号任务执行, 执行代号:狂风; 定时时间:7s");}},7000);timer.schedule(new TimerTask() {@Overridepublic 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就是系统时间加上延迟延迟时间}@Overridepublic 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() {@Overridepublic void run() {System.out.println("玄武一号任务执行, 执行代号:闪电; 定时时间:10s");}},10000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("朱雀二号任务执行, 执行代号:暴风; 定时时间:7s");}},7000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("青龙三号任务执行, 执行代号:狂风; 定时时间:5s");}},5000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("白虎四号任务执行, 执行代号:地震; 定时时间:3s");}},3000);System.out.println("主线程任务执行 未定时"); //主线程main直接执行}
需要注意理解的是:这段代码一共有两个线程在同时执行,main线程就是那个添加任务的线程,t线程一直就做一件事,就是在循环里一直扫描判断是否需要执行任务