定时器的实现及使用

10 篇文章 0 订阅
1 篇文章 0 订阅

目录

前言

标准库的定时器

 实现一个定时器

设计思路

代码思路


前言

定时器就类似于定闹钟,我们平时的闹钟,有两种风格:

  1. 指定特定时刻提醒
  2. 指定特定时间段后提醒

我们所说的定时器,不是提醒,而是执行一个事先准备好的代码/方法.定时器是我们日常开发中一个常用的组件,尤其是针对网络编程的时候,很容易出现卡顿,连接不上的情况,此时我们就可以使用定时器,来进行及时止损.

标准库的定时器

和阻塞队列类似,Java标准库里,也为我们提供了定时器.

这个Timer类就是标准库给我们提供的定时器.

调用schedule方法就可以实现定时器的效果.schedule翻译过来就是安排的意思. 

schedule方法有两个参数,

第一个参数new TimerTask()其实就是个runnable,指明任务的内容,第二个参数,指定时间,在多少毫秒后,执行任务.

运行这样一段代码

 

可以看到代码的执行顺序是正确的.


 实现一个定时器

设计思路

实现一个定时器,有两个问题需要解决:

1.让被注册的任务,能够在指定时间,被执行

2.一个定时器可以注册N个任务,N个任务会按照最初的约定时间,按顺序执行.

针对第一个问题:我们可以单独在定时器内部,实现一个线程.让这个线程周期性的扫描任务列表,判定任务是否是到时间了,如果到时间了,就执行任务;没到时间,就在等等.

针对第二个问题:我们首先要考虑的是应该用哪种数据结构来保存这N个任务比较合适.思考一下,我们不难发现,在此场景下,使用优先级队列,是一个很好的选择!因为任务列表里的每个任务,都带有一个时间(表示任务在多久之后被执行),一定是时间越靠前的,越先被执行.那么我们可以按照时间小的,作为优先级高的,此时队首元素,就是整个任务队列中,最先被执行的任务.

我们采用了优先级队列后,扫描线程就无需每次都遍历任务列表,只需要扫描队首元素即可.(因为如果队首元素的执行时间还没到,后续元素不可能到执行时间)

经过上述讨论,总结出定时器里的两个核心:

1.有一个扫描线程,负责判定时间到没到

2.有一个数据结构,来保存所有被注册的任务.(使用一个优先级队列来表示)

值得注意的是:此处的优先级队列,会在多线程的环境下使用.(很明显,调用schedule是一个线程,扫描是另一个线程),所以我们还要关注线程安全问题. 因此,标准库提供的PriorityBlockingQueue是很好的选择.

代码思路

 任务的表示,我们自定义一个MyTask类.里面包含了要被执行的任务,和任务执行的时间.

 定时器里,还需要一个"schedule"方法,来注册任务

schedule本身比较简单,只是单纯的把任务放到队列里了. 

定时器真正麻烦在扫描线程的工作上:

阻塞队列,只能先把元素出队列,才好判定,不满足还要放回队列当中去,不像普通队列,可以直接取队首元素判定.


上述代码,是不完整的,里面还包含两个问题:

1.没有指定MyTask的比较规则

2.忙等问题

针对第一个问题:可以让我们的MyTask类实现Comparable接口,重写compareTo方法来实现.(也可以使用Comparator单独写一个比较器).


针对第二个问题:

"忙等":时间没到, 就会一直重复做取出来塞回去的操作.

这种现象就叫做"忙等",等但是没闲着.

正常情况下,等待就是要释放掉CPU资源,让CPU干别的事情.但是忙等,即进行了等待,还占用着CPU资源.

 注意:我们的队列是优先级队列(堆实现),put放回去,会触发优先级调整(堆的向上调整),调整之后,myTask又回到队首了,下次循环取出的任务还是这个任务.

假设现在是13:00,而队首元素的任务时间是14:00执行,取出的元素,是不能执行的,那么在这个时间段里,这个循环可能要执行数以十亿次.

像忙等这样的情况,也是需要辩证看待的.但是在当前的场景下,确实是不太好的(无意义的占用CPU资源).有的情况下,忙等,也是一个好的选择.所以要根据场景辩证看待"忙等".

所以,上述场景下,针对上述代码,就不再进行忙等了,而是进行"阻塞式"等待.

那么问题又来了,阻塞式等待,我们是采用sleep还是wait呢??

此处的等待,如果当前是13:00,任务执行时间是14:00,那么等待一个小时就行了.此处看似等待时间是明确的,但实际上并不明确.因为随时可能有新任务的到来(随时可能有线程调用schedule方法添加新任务,添加的新任务的时间,可能早于当前队首元素的时间).

所以使用sleep,就不太合适了,如果使用sleep,那么在sleep等待过程中,添加的新任务是13:30的,此时依旧按照sleep一个小时来等待,那么这个新任务就被错过了.

因此,使用wait更加合适,能够方便随时唤醒等待.

使用wait等待,每次新任务来了(有人调用schedule),就notify一下,重新检查一下时间,重新计算要等待的时间,并且,wait也提供了一个带有"超时时间"的版本.

扫描线程里:

schedule方法里:


上述代码经过改动以后,还存在一个很严重的关于线程安全/随机调度的问题.

我们考虑一个极端情况:

 

 假设代码执行到put这一行,这个线程就从cpu上调度走了,当线程回来之后,接下里就要进行wait操作了,此时wait的时间已经算好了.比如curTime是13:00,任务getTime是14:00,即将要wait一个小时(此时还没有执行,当前线程就从cpu上调度走了).

此时另一个线程调用了schedule,添加了新任务,新任务的执行时间是13:30.

 此处调用schedule会执行notify通知wait唤醒.但由于扫描线程中的wait还没有执行到呢,所以此处的notify只是"空打一炮",不会产生任何的唤醒操作!!!

此时此刻,新的任务已经插入到了队列当中,新的任务由于时间最小,所以调整到了队首,紧接着扫描线程回到了cpu,但此时的等待时间还是一个小时.故,13:30的新任务就被错过了!!!

了解了上述问题之后,我们发现,造成上述问题的原因,是因为take操作和wait操作并非是原子的.如果将take和wait加上锁,保证在这个过程中,不会有新任务插入进来.问题就解决了.

(只要保证每次notify时,确实是在wait)


完整代码:

import java.util.concurrent.PriorityBlockingQueue;

//使用这个类来表示定时器中的任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务
    private Runnable runnable;
    //任务执行的时间(使用毫秒时间戳表示)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }
    //获取当前任务的时间
    public long getTime(){
        return time;
    }
    //执行任务
    public void run(){
        runnable.run();
    }
    @Override
    public int compareTo(MyTask o) {
        //返回小于0,大于0,0
        //this 比 o 小,返回 < 0
        //this 比 o 大,返回 > 0
        //this 和 o 相等,返回 0
        //当前要实现的效果,是队首元素是时间最小的任务
        //所以是this.time - o.time
        return (int) (this.time-o.time);
    }
}

//自己实现一个定时器
class MyTimer{
    //扫描线程
    private Thread t = null;

    //有一个阻塞优先级队列,来保存任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer(){
        t = new Thread(()->{
            while (true) {
                try {
                    //取出队首元素,检查查看队首元素的任务是否到时间了
                    //如果时间没到,就把任务在塞回到队列当中去
                    //如果时间到了,就执行任务
                    synchronized (this) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime < myTask.getTime()){
                            //还没到时间,不必执行
                            //现在是12:00,取出来的任务是 14:00 执行
                            queue.put(myTask);
                            //在put之后,进行一个wait
                                this.wait(myTask.getTime() - curTime);
                        } else {
                            //时间到了,执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
    }

    //第一个参数表示 任务的内容
    //第二个参数表示 任务在多少毫秒之后被执行
    public void schedule(Runnable runnable,long after){
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized (this) {
            this.notify();
        }
    }
}

public class ThreadDemo25 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }

}

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值