Java EE:2.多线程-初阶(第九弹):多线程案例-定时器
目录
2.定时器中,能够管理多个任务的,必须使用一些集合类把这多个任务给管理起来
书接上文:Java EE:2.多线程-初阶(第八弹)~~
8.4定时器
定时器是什么
定时器类似“闹钟”,时间到了,执行一些逻辑~~
闲聊:定时器这东西各种语言都有~~
但是C语言除外,C语言摆烂太多年了,能提供的基础设施非常有限,就连遍历个目录啥的,C语言自身都做不到,因此肯汤姆逊才去搞了个Go,弥补一些不足~~
定时器是一种实际开发中非常常用的组件
比如网络通信中,如果对方500ms内没有返回数组,则断开连接尝试重连
再比如说一个Map,希望里面的某个Key在3s之后过期(自动删除),也都需要定时器
标准库中的定时器
标准库中提供了一个 Timer 类,Timer类的核心方法为 schedule
schedule 包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)
package thread; import java.util.Timer; import java.util.TimerTask; public class Demo37 { public static void main(String[] args) { Timer timer=new Timer(); //这个任务会在2000毫秒后执行 timer.schedule(new TimerTask(){ @Override public void run() { System.out.println("hello timer"); } },2000); System.out.println("hello main"); } }2s钟后的效果👇
我们正常描述任务,是 Runnable
在定时器这里稍微特殊一点,把 Runnable 封装成了 TimerTask
我们进入 TimerTask 内部查看源码会发现,其实还是继承了 Runnable,核心还是重写 run 方法👇
当然,定时器中不只可以添加一个任务,也可以添加多个任务👇
package thread; import java.util.Timer; import java.util.TimerTask; public class Demo37 { public static void main(String[] args) { Timer timer=new Timer(); timer.schedule(new TimerTask(){ @Override public void run() { System.out.println("hello 3000"); } },3000); timer.schedule(new TimerTask(){ @Override public void run() { System.out.println("hello 2000"); } },2000); timer.schedule(new TimerTask(){ @Override public void run() { System.out.println("hello 1000"); } },1000); System.out.println("hello main"); } }可以看到,三个任务在依次执行👇
答疑:
Q1:程序好像也没有正常结束
因为和线程池一样,Timer中也包含前台线程,阻止进程结束
Q2:抽象类不是不能创建对象吗??
咱们这个不是抽象类,咱这个是匿名内部类~~
因为咱 new 的不是 TimerTask ,咱 new 的是 TimerTask 的子类,只不过这个子类的名字叫什么?不知道,是一个匿名的,这个匿名内部类的写法在一开始讲线程的创建方法时就说过了👉Java EE:2.多线程-初阶(第一弹)
这个代码做了三件事:
1.创建了子类(父类是 TimerTask),子类是匿名的
2.重写了 run
3.new 了子类的实例
实现定时器
1.创建一个类,表示一个任务
2.定时器中,能够管理多个任务的,必须使用一些集合类把这多个任务给管理起来
思考:那什么集合类来管理呢?
ArrayList❌
当管理多个任务的时候,需要确保,时间最早的任务,最下执行
用 ArrayList 就要通过遍历的方式来找到时间最早,就麻烦了
哈希❌
每个任务都是带时间的,我们就需要根据时间来找到任务,哈希也不太方便
队列❌
普通的队列肯定不行,因为通过上述代码能发现,我们添加任务的顺序和执行任务的顺序不一定一样,添加的时候是3→2→1的顺序,执行的时候就是1→2→3
优先级队列✔
我们可以根据时间来定义优先级,定义一个小根堆,让时间小的跑到队首去
阻塞队列❓
阻塞队列也提供了带优先级的版本,要不要使用,咱后续再说~~
当写到这里时,我们说它是不完整的,因为必须要明确比较规则
如果你的元素是 数字 / String 这种本身就有明确比较规则的对象,可以不额外指定
但现在是把自己定义的类,传进去
下面用实现 Comparable 的 compareTo 方法进行实现
(对于Comparable、Comparator不明白的可以参考:Comparator、Comparable接口及compare、compareTo总结)
@Override public int compareTo(MyTimerTask o) { //我们的需求是让时间最小的元素在队首 //=0,说明this和o相等 //<0,说明this<o //>0,说明this>o //数学上比较两个数字的大小:作差 //下面这种情况谁减谁?? return (int)(this.time-o.time); return (int)(o.time-this.time); //千万不要背!!背就一定会出错!! //咱就做实验,一个不行换另一个 }3.实现 schedule 方法,把任务添加到队列中即可
将前三步搞定后的代码👇
package thread; //写法一:基于抽象类的方式定义 MyTimerTask //这样的定义虽然可以,但写起来有点麻烦 //abstract class MyTimerTask implements Runnable{ // @Override // public abstract void run();//后续再实现run的逻辑 //} import java.util.PriorityQueue; //写法二:持有成员 task 的方式 class MyTimerTask implements Comparable<MyTimerTask>{ private Runnable task; //记录任务要执行的时刻 private long time; public MyTimerTask(Runnable task,long time){ this.task=task; this.time=time; } @Override public int compareTo(MyTimerTask o) { //我们的需求是让时间最小的元素在队首 //=0,说明this和o相等 //<0,说明this<o //>0,说明this>o //数学上比较两个数字的大小:作差 //下面这种情况谁减谁?? return (int)(this.time-o.time); //return (int)(o.time-this.time); //千万不要背!!背就一定会出错!! //咱就做实验,一个不行换另一个 } } //自己实现一个定时器 class MyTimer{ private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>(); //任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask t=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(t);//不是阻塞队列,不用put } } public class Demo38 { public static void main(String[] args) { } }4.额外创建一个线程,负责执行队列中的任务
注意:这个和线程池不同,线程池是 只要队列不为空,就立即取任务并执行
此处需要看队首元素的时间,是否到了,时间到→才能执行,时间不到→不能执行
细节:此处我们叫“时刻”,而不是“时间”
时间:时间段(比如1个小时5分钟)
时刻:一个瞬间
计算机中,使用时间戳来表示“时刻”
啥是时间戳??
我们平常所看到的时间,都是格式化时间,是给人看的👇
给计算机看的,叫时间戳👇
这个时间戳是以1970年1月1日0时0分0秒作为基准时刻,计算当前时刻和基准时刻的秒数(毫秒/微秒)之差
当时我们学C语言的时候,讲到 if 的时候,写了猜数字=>随机数=>使用时间戳作为随机种子
package thread; //写法一:基于抽象类的方式定义 MyTimerTask //这样的定义虽然可以,但写起来有点麻烦 //abstract class MyTimerTask implements Runnable{ // @Override // public abstract void run();//后续再实现run的逻辑 //} import java.util.PriorityQueue; //写法二:持有成员 task 的方式 class MyTimerTask implements Comparable<MyTimerTask>{ private Runnable task; //记录任务要执行的时刻 private long time; public MyTimerTask(Runnable task,long time){ this.task=task; this.time=time; } @Override public int compareTo(MyTimerTask o) { //我们的需求是让时间最小的元素在队首 //=0,说明this和o相等 //<0,说明this<o //>0,说明this>o //数学上比较两个数字的大小:作差 //下面这种情况谁减谁?? return (int)(this.time-o.time); //return (int)(o.time-this.time); //千万不要背!!背就一定会出错!! //咱就做实验,一个不行换另一个 } public long getTime(){ return time; } public void run(){ task.run(); } } //自己实现一个定时器 class MyTimer{ private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>(); //任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask t=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(t);//不是阻塞队列,不用put } //构造方法 public MyTimer(){ //创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ while(true){ //先判定队列是否为空 if(queue.isEmpty()){ continue; } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 continue;//先不执行 }else{ //时间到了,执行任务 task.run(); queue.poll(); } } }); t.start(); } } public class Demo38 { public static void main(String[] args) { } }写到这里时,我们是没有考虑线程安全问题的
当前调用 schedule 是一个线程,定时器内部又有一个线程,多个线程操作同一个队列,一定涉及到线程安全问题
下面我们给上述代码进行加锁👇
①先创建个锁对象:
//直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了 private Object locker=new Object();//加锁处理,创建锁对象如果使用 this 加锁,this.wait()、this.notify()效果是一样的~~
答疑:在这的两个 this 是一个吗?一个不是内部类的吗?
lambda 也是能捕获到外部类的 this 的
虽然看起来这个 this 是指向匿名内部类,但实际上我们写的是一个 lambda,捕获到的是外部类的 this,而不是内部类的 this
所以也证明了 变量捕获不是只能捕获常量,this指向的是不能修改的,当然也能捕获~~
②给 schedule 加锁:
//任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ synchronized (locker){ //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(timerTask);//不是阻塞队列,不用put } }其实写成这样也可以👇,但是写到外面的话可能会因为 synchronized 阻塞,使得时间可能会存在一点误差
//任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay); synchronized (locker){ //将任务对象放入任务队列 queue.offer(timerTask);//不是阻塞队列,不用put } }因为 timerTask 是马上就能执行到(获取了一次系统时间),然后可能在获取锁的时候耽误一点时间,导致锁内部的时间与刚刚获取到的时间不一致,因此我们就以入队列这个时刻作为时间基准了,用锁全包起来
③给构造对象 MyTimer 里的线程加锁
答疑:给构造方法加锁??
构造方法本身可以写 synchronized ,但是这个地方不能这么写,因为要保护的逻辑在线程的 run 里头(lambda),和构造方法是两个不同的方法~~
//构造方法 public MyTimer(){ //创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ while(true){ synchronized (locker){ //先判定队列是否为空 if(queue.isEmpty()){ continue; } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 continue;//先不执行 }else{ //时间到了,执行任务 task.run(); queue.poll(); } } } }); t.start(); }此时我们发现上述4个步骤就写的差不多了👇
package thread; //写法一:基于抽象类的方式定义 MyTimerTask //这样的定义虽然可以,但写起来有点麻烦 //abstract class MyTimerTask implements Runnable{ // @Override // public abstract void run();//后续再实现run的逻辑 //} import java.util.PriorityQueue; //写法二:持有成员 task 的方式 class MyTimerTask implements Comparable<MyTimerTask>{ private Runnable task; //记录任务要执行的时刻 private long time; public MyTimerTask(Runnable task,long time){ this.task=task; this.time=time; } @Override public int compareTo(MyTimerTask o) { //我们的需求是让时间最小的元素在队首 //=0,说明this和o相等 //<0,说明this<o //>0,说明this>o //数学上比较两个数字的大小:作差 //下面这种情况谁减谁?? return (int)(this.time-o.time); //return (int)(o.time-this.time); //千万不要背!!背就一定会出错!! //咱就做实验,一个不行换另一个 } public long getTime(){ return time; } public void run(){ task.run(); } } //自己实现一个定时器 class MyTimer{ private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>(); //直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了 private Object locker=new Object();//加锁处理,创建锁对象 //任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ synchronized (locker){ //以入队列这个时刻作为时间基准 //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(timerTask);//不是阻塞队列,不用put } } //构造方法 public MyTimer(){ //创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ while(true){ synchronized (locker){ //先判定队列是否为空 if(queue.isEmpty()){ continue; } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 continue;//先不执行 }else{ //时间到了,执行任务 task.run(); queue.poll(); } } } }); t.start(); } } public class Demo38 { public static void main(String[] args) { } }④接下来我们在 main 方法里写个方法验证一下定时器效果
public class Demo38 { public static void main(String[] args) { MyTimer timer=new MyTimer(); //添加任务 timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 3000"); } },3000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 2000"); } },2000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 1000"); } },1000); } }我们看起来代码确实是能够正常执行了👇
其实我们的代码写到这儿,还有一严重的问题,需要进一步考虑:
一旦这个程序跑起来,电脑风扇就会狂转~~
因为 CPU温度升高=>CPU在高负荷工作,引起这个问题的核心原因就在下述代码👇
//创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ while(true){ synchronized (locker){ //先判定队列是否为空 if(queue.isEmpty()){ continue; } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 continue;//先不执行 }else{ //时间到了,执行任务 task.run(); queue.poll(); } } } });
这里的转,其实是在等待新的任务到来,明明是等,却需要消耗大量的CPU资源,这是不太科学的
这种通过CPU不停的死循环式等待,在计算机中称为“忙等”(忙了一天,又没啥实质性的产出~~)
如何解决呢??
sleep❌
如果设置的短,起不到解决忙等的效果
如果设置的长,万一有新的任务来了,又没法及时处理~~(如果你 sleep(2000),这时候你 schedule 1000,那这个 1000 的就响应不了了)
wait notify✔
这个才是正确的解决方式,我们可以按需等待,按需唤醒~~
我们只需要去 wait 就可以了,等到其他有线程添加任务的时候再去把它唤醒~~
//构造方法 public MyTimer(){ //创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ try{//将wait的异常处理放到最外面了 while(true){ synchronized (locker){ //先判定队列是否为空 while(queue.isEmpty()){//换成wait之后,这里要把if改成while,防止出现Bug //这里的 sleep 时间不好设定!! //Thread.sleep(); //continue; locker.wait(); } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 continue;//先不执行 }else{ //时间到了,执行任务 task.run(); queue.poll(); } } } }catch (InterruptedException e){ e.printStackTrace(); } }); t.start(); }再写唤醒操作:队列不为空,就唤醒~~
public void schedule(Runnable task,long delay){ synchronized (locker){ //以入队列这个时刻作为时间基准 //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(timerTask);//不是阻塞队列,不用put locker.notify();//唤醒操作 } }在处理完 忙等 的问题之后,其实还有一处问题:就出现在这儿👇
假设当前时刻是14:00,队首的任务时刻是14:30,时间还没到,在上述代码中就会执行continue,啥也不干,直接回到循环那里,继续执行 while 判断,继续执行 wait ,继续执行 if 判断,意味着这整个逻辑又会出现快速的循环👇
这就相当于我中午睡觉,14:30起床,闹钟响了,一看:14:00,好,我趴下,我再起来看:14:00,好,我再趴下,我再起来看:14:00,好,我再趴下,我再起来看:14:01……那我这个觉就睡了个寂寞,全在看时间呢,也属于忙等,这个过程虽然也是在等,但我也没闲着,完全起不到任何休息的作用
答疑:用 goto
goto 是汇编(机器指令)无条件跳转指令~~Java中没有 goto ,其他语言(C/C++)中的 goto 都被喷成🐶了,因为 goto 会搞出很多骚操作~~一个代码中如果有很多 goto ,代码就会变成意大利面条~~
因此我们也需要用到 wait ,直接这么 wait 吗👇??
其实这并不合适,因为这个 wait 必须得有其他线程 schedule 才能唤醒,实际上,此处是应该时间到,就继续执行的!!!
因此我们不能用 wait 死等,而是给 wait 设置超时时间,这个超时时间就是时间差👇
if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 //continue;//先不执行 locker.wait(task.getTime()-System.currentTimeMillis());比如当前时刻是14:00,队首的任务时刻是14:30,那么此处就是 wait 30分钟
此时我们思考另一个问题:比如 wait 30分钟,如果才过了10分钟(14:10),恰好有一个新的任务来了,调用 schedule ,添加了15:00的任务,那么 schedule 中的 notify 不就把这里的 wait 给唤醒了吗?(相当于才休息10分钟,就被唤醒了~~)(notify 不是只能唤醒不带参数的 wait ,而是都能唤醒的~~)
其实这个无关紧要的~~无非就是再次判定队首元素的任务时间(14:30)和当前时刻(14:10),再次 wait 20分钟呗~~(多执行一次循环,无所谓,主要是怕短时间内进行大量的循环)
答疑:用 sleep 可以吗??
绝对不行的!比如说,本来队首是14:30分,咱们在14:10分添加一个新的任务14:20来执行
如果刚才是 sleep ,sleep 30分钟,无法被唤醒,意味着 14:20的任务(新任务)无法及时执行了,而且 sleep 的时候,不会释放锁的!!(sleep 抱着锁睡着了~~)
这就导致另一个线程调用 schedule 阻塞在加锁的逻辑上~~
闲聊:
1.多线程考虑真多
那确实,因为咱们是要在无数种情况下都要打败灭霸~~势必要多思考~~
2.随便写写就是线程安全问题
不充分理解线程安全,很难正确编写多线程程序
正因为多线程这么难,有的语言直接就把多线程给干掉了!!!
JS:JS只是提供了“定时器”,凑合实现一些并发效果
Python:虽然提供了线程,但Python的线程是“假的线程”
Python的全局解释器锁,使得Python的多线程本质上是“串行执行”的
3.JS的异步操作呢?
底层是多线程,但是封装起来了,让你无法灵活操作
只能 async、await ,操作简单,不容易出错~~
答疑:那这段代码👇的 if 不改成 while 吗?这里也是有 wait 的呀
这里就不建议使用 while 了,虽然 wait 和 while 不分家,但也要分情况
使用 while 就是为了“再次确认,条件是否满足”
当前 wait 唤醒之后,通过总的 while 循环刚好就是再走一遍确认逻辑~~
而且,这里把 if 改成 while 之后,你让下面的 else 情何以堪~~
但是一开始的 while 还是要加的👇
因为下面的操作就开始操作队列了,队列为空,那不就麻烦了嘛,是要出大问题的~~
闲聊:终于知道业务时大部分用 wait 而不是 sleep了
确实,wait 比 sleep 更多
基于堆(优先级队列)实现的定时器完整代码👇
package thread; //自己基于堆(优先级队列)实现的定时器 //写法一:基于抽象类的方式定义 MyTimerTask //这样的定义虽然可以,但写起来有点麻烦 //abstract class MyTimerTask implements Runnable{ // @Override // public abstract void run();//后续再实现run的逻辑 //} import java.util.PriorityQueue; //写法二:持有成员 task 的方式 class MyTimerTask implements Comparable<MyTimerTask>{ private Runnable task; //记录任务要执行的时刻 private long time; public MyTimerTask(Runnable task,long time){ this.task=task; this.time=time; } @Override public int compareTo(MyTimerTask o) { //我们的需求是让时间最小的元素在队首 //=0,说明this和o相等 //<0,说明this<o //>0,说明this>o //数学上比较两个数字的大小:作差 //下面这种情况谁减谁?? return (int)(this.time-o.time); //return (int)(o.time-this.time); //千万不要背!!背就一定会出错!! //咱就做实验,一个不行换另一个 } public long getTime(){ return time; } public void run(){ task.run(); } } //自己实现一个定时器 class MyTimer{ private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>(); //直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了 private Object locker=new Object();//加锁处理,创建锁对象 //任务 task 以当前时刻为基准,delay 毫秒后执行 public void schedule(Runnable task,long delay){ synchronized (locker){ //以入队列这个时刻作为时间基准 //创建一个任务对象 //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳 MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay); //将任务对象放入任务队列 queue.offer(timerTask);//不是阻塞队列,不用put locker.notify();//唤醒操作 } } //构造方法 public MyTimer(){ //创建一个线程,负责执行队列中的任务 Thread t=new Thread(()->{ try{//将wait的异常处理放到最外面了 while(true){ synchronized (locker){ //先判定队列是否为空 while(queue.isEmpty()){//换成wait之后,这里要把if改成while,防止出现Bug //这里的 sleep 时间不好设定!! //Thread.sleep(); //continue; locker.wait(); } //取出队首元素 MyTimerTask task=queue.peek(); if(System.currentTimeMillis()<task.getTime()){ //当前任务时间,如果比系统时间大,说明任务执行的时机未到 //continue;//先不执行 locker.wait(task.getTime()-System.currentTimeMillis()); }else{ //时间到了,执行任务 task.run(); queue.poll(); } } } }catch (InterruptedException e){ e.printStackTrace(); } }); t.start(); } } public class Demo38 { public static void main(String[] args) { MyTimer timer=new MyTimer(); //添加任务 timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 3000"); } },3000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 2000"); } },2000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 1000"); } },1000); } }
标准库提供的 Timer 和自己写的 MyTimer 差不多
都是使用一个线程,负责扫描队首元素,并执行的
①如果任务少/任务时间分散,都无所谓
②如果任务特别多/时间非常集中呢?一个线程就可能执行不过来~~
因此我们也需要结合线程池来使用~~
比如我一口气注册了10000个任务,每个任务都是14:00来执行的,此时就完全可以创建多个线程,负责执行这里的队列中的任务~~
一个线程负责扫描,扫描到需要执行的任务,添加到另一个线程池的任务队列中,由多个线程负责执行~~这样的做法,在Java标准库中也是由体现的,就是这样的代码👇
Executors.newScheduledThreadPool(4);这样的操作,就是创建了一个带有线程池的定时器
课件内容:实现定时器
定时器的构成
①一个优先级队列(不要使用 PriorityBlockingQueue,容易死锁!)
②队列中的每个元素是一个 Task 对象
③Task 中带有一个时间属性,队首元素就是即将要执行的任务
④同时有一个 worker 线程一直扫描队首元素,看队首元素是否需要执行
1.Timer 类提供的核心接口为 schedule ,用于注册一个任务,并指定这个任务多长时间后执行
public class MyTimer { public void schedule(Runnable command, long after) { // TODO } }2.Task 类用于描述一个任务(作为 Timer 的内部类),里面包含一个 Runnable 对象和一个 time (毫秒时间戳)
这个对象需要放到 优先级队列 中,因此需要实现 Comparable 接口
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; } @Override public int compareTo(MyTask o) { // 这样的写法意味着每次取出的是时间最小的元素. // 到底是谁减谁?? 俺也记不住!!! 随便写一个, 执行下, 看看效果~~ return (int)(this.time - o.time); } }3.Timer 实例中,通过 PriorityQueue 来组织若干个 Task 对象
通过 schedule 来往队列中插入一个个 Task 对象
class MyTimer { // 核心结构 private PriorityQueue<MyTask> queue = new PriorityQueue<>(); // 创建一个锁对象 private Object locker = new Object(); public void schedule(Runnable command, long after) { // 根据参数, 构造 MyTask, 插入队列即可. synchronized (locker) { MyTask myTask = new MyTask(runnable, delay); queue.offer(myTask); locker.notify(); } } }4.Timer 类中存在一个 worker 线程,一直不停的扫描队首元素,看看是否能执行这个任务
所谓“能执行”指的是该任务设定的时间已经到了
// 在这里构造线程, 负责执行具体任务了. public MyTimer() { Thread t = new Thread(() -> { while (true) { try { synchronized (locker) { // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素. while (queue.isEmpty()) { locker.wait(); } MyTask myTask = queue.peek(); long curTime = System.currentTimeMillis(); if (curTime >= myTask.time) { // 时间到了, 可以执行任务了 queue.poll(); myTask.runnable.run(); } else { // 时间还没到 locker.wait(myTask.time - curTime); } } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); }
基于“时间轮”的定时器
定时器,除了基于堆(优先级队列)方式来实现的定时器之外,还有一种方案,基于“时间轮”
①类似搞个循环队列(数组)
②每个元素是一个“时间单位”
③每个元素又是一个链表
④每到一个时间单位,光标指向下一个元素,同时把这个元素上对应链表中的任务都执行一遍……
优势:性能更高(就是往下走一个格,速度很快),更适合任务特别多的情况
劣势:时间精度不如优先级队列(优先级队列,最大的问题在于堆的调整,是logN的,因此更适合精度高的情况)
重点还是在于掌握优先级队列的定时器:
①代码中涉及到的锁操作、wait-notify操作
②忙等 部分的讨论
由于定时器,是一个非常重要的组件,因此会在分布式系统中,把定时器专门提取出来,封装成一个单独的服务器(和消息队列很像)
分布式系统,有很多服务器
如果只是一个类,意味着所有的服务器都需要执行这一套同样的逻辑,比较复杂,如果进行整改,所有的都要改
因此提取出来,形成独立的服务器,大家都去调用,如果未来有升级调整,也便于分配给这个定时任务服务器单独的硬件资源~~
闲聊:
简历上不是“烂大街”的项目不能写,而是你“一知半解”就往上写~~
如果有同学真的能把这个项目吃透,那么面试官会认为这个同学完全Ok的1.愿意主动学习
2.自学能力很强
如果你简历上写一堆东西,结果让你介绍一下:
项目的登录逻辑是怎样实现的
会话是如何存储的
会话过期策略是怎样设定的
……
你答不上,面试官只能认为,你学的火候太差了~~
像这种技术面试,面试官是可以通过简单的几个问题,就摸出你的老底儿~~
答疑:
Q1:一般做项目是1~2个吗?
多多益善,能做10个8个的最好不过了~~多一个项目,多一分容错~~
项目多了,更大概率打动面试官(每个项目都得认真做,面试官绝对不傻,不好糊弄~~)
Q2:到大三也做不了这么多啊
10个8个确实有点夸张,搞个3个、4个、5个还是很有可能的~~在于数量、质量
Q3:啥是微服务??
在后续学完Java EE进阶后,就会去接触 Spring Cloud(微服务),在Java圈子里,谈到微服务,基本等价于 Spring Cloud,到时候会学一些 nacos、openfeign、gateway操作……
微服务不是微信服务,是一个“噱头”
咱们同学以后工作中,大部分同学是接触不到微服务的
微服务只有中大厂才会涉及到
对于大部分的中小公司来说,单机程序/简单的分布式系统 足以应付业务场景
服务器,最初,一个服务器程序处理所有工作
后来分成3、4个服务器配合=>分布式
后来分成20个、30个服务器配合=>微服务
中大厂这种规模比较大的团队,微服务才能真正发挥威力~~
目前很多同学把“微服务”奉为至宝、吊炸天的技术~~
殊不知,任何技术,确实能解决问题,也都要付出代价~
更多推荐

























所有评论(0)