hello,今天为大家带来定时器 的实现
定时器是用带有优先级的阻塞队列实现的(也就是带有阻塞功能的小根堆)
定时器是多线程中让线程更加高效的执行的手段,,就是时间到了,让该任务执行,在Java标准库中有自己的实现,Timer类,它的核心方法是schedule,下面来看看它的具体代码
1.标准库的实现
import java.util.TimerTask; //定时器 import java.util.Timer; public class ThreadDemo5 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello4"); } }, 4000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello3"); } }, 3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello2"); } }, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello1"); } }, 1000); System.out.println("hello0"); } }
我在学习这部分的时候,我产生了一个巨大的疑问困扰了我好几天,我在想这个定时器到底有几几个线程,想了好久我想通了,其实这就是一个线程,而schedule方法中写的是线程要执行的任务
一个线程可以执行多个任务,而Timer类内置了一个前台线程
因此,是只有一个线程和多个任务
下面,我们来进行定时器的实现
定时器就保证了多个线程有序的执行,那么有细心的老铁就发现了,这个运行一直没有结束,是为啥呢?
因为Timer类内置了一个线程,这个线程是前台线程,我们知道前台线程决定了线程是否结束,而前台线程会阻止线程的结束,所以代码会一直运行
重点来了,这个标准库自带的版本很简单,我们要自己咋样实现一个定时器呢?
需要一个带优先级的阻塞队列
2.自己实现定时器
import java.util.concurrent.PriorityBlockingQueue; /** * Created with IntelliJ IDEA. * Description: * User: WHY * Date: 2023-03-23 * Time: 15:28 */ //自己实现一个定时器 //表示一个执行的任务 class MyTask implements Comparable<MyTask>{//实现堆就要写比较规则,根据啥比较的 public Runnable runnable;//这里是runnable类型的是因为根据源码写的 public long time;//任务执行的绝对时间 public MyTask(Runnable runnable,long delay){ this.runnable=runnable; this.time=System.currentTimeMillis()+delay;//绝对时间戳=(当前时间-基准时间)+任务多久后执行的时间 } @Override public int compareTo(MyTask o) { return (int)(this.time-o.time); } } class MyTimer{ //创建带有阻塞功能的优先级阻塞队列 private Object locker=new Object(); private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>(); public void schedule(Runnable runnable,long delay){ //根据参数,构造任务,插入队列中 MyTask myTask=new MyTask(runnable,delay); queue.put(myTask); 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{ //时间还没到,所以需要将拿出的队列放回去 //put和take方法带有阻塞功能,peek没有,所以不用 queue.put(myTask); locker.wait(myTask.time-curTime); } } } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); } } public class ThreadDemo4 { public static void main(String[] args) { MyTimer myTimer=new MyTimer(); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 4"); } },4000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 3"); } },3000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 2"); } },2000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 1"); } },1000); } }
经过验证,代码很好的执行了多个任务
写这个代码要注意:
1.堆的实现一定要写比较规则
2.会出现忙等现象(解决办法加wait和notify)
这个代码中的wait和notify的作用是啥呢
当目前队首元素不满足条件,就重新塞回去,然后阻塞等待,为啥呢,因为当在schedule方法中创建出新任务时如果这个任务时间符合条件,那就进入线程执行,那么就在创建新的任务的时候唤醒wait,让线程重新进入循环(重点理解)!!!
为啥加锁一定要写到整个try那里,不能写到wait那里吗,我们来分析一下
我们都知道,线程的调度是随机的. 假设t1在执行的过程中,执行到queue.put()方法即将执行wait时,t2开始执行,现在来了一个时间为14:10的任务,然后插入了队列中,然后进行notify,这个时候的notify相当于空打一炮,此时都还没有wait,notify就唤醒了个寂寞,然后现在t1执行wait,注意,在执行wait时就已经解锁并且阻塞等待了,此时进行相减的是14:30-14:30,那么14:10分的任务就被错过了,所以这就是一直等,错过了,那么如果写成对整个try语句加锁,就不一样了
也就是说假设任务时间是14:30,目前时间是14:00,那么现在取出这个任务,判断大小,发现不符合,所以要放回去,然后现在把14:30放回去了,即将wait的时候,t2开始执行,取出14:10分的任务,进行唤醒操作,但是现在t1还没有执行到t1的wait,所以唤醒就没有用了.那么任务时间就还是14:30,没有被更新为14:10,t1继续执行wait,此时任务时间还是14:30,所以错过过14:10分的任务,也就是t2更新的任务t1没有接收到
现在这一段操作都是原子的,t2想执行也要等到t1释放锁,所以是线程安全的
这个代码比较复杂,具体注释写在代码旁边了,需要的老铁可以看一看
今天的讲解就到这里,我们下期再见