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释放锁,所以是线程安全的
这个代码比较复杂,具体注释写在代码旁边了,需要的老铁可以看一看
今天的讲解就到这里,我们下期再见