定时器的简单使用和实现

定时器

    • 什么是定时器
    • 标准库中的定时器
      • 使用
    • 定时器的实现

什么是定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

标准库中的定时器

标准库中提供了一个Timer类, java.util.Timer

使用

Timer 类的核心方法为 schedule().

schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("3000");
    }
}, 3000);

这种写法很像Runnable的写法, 实际上Time类实现了Runnable接口

上述代码的意思就是在3000ms之后, 执行run()方法. 但run()方法不是在调用schedule()方法的那个线程中执行的, 而是在Timer内部的线程中执行的. 并且为了保证随时可以处理新安排的任务, 这个线程会持续执行, 并且该线程是前台线程, 此线程不结束, 那么该进程就不会结束.

定时器的实现

一个定时器里是可以有多个任务的

MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("3");
    }
}, 3000);
timer.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("2");
    }
}, 2000);
timer.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("1");
    }
}, 1000);

先要能够把一个任务描述出来, 然后再用合适的数据结构将多个任务组织起来

步骤:

  1. 创建一个TimerTask这样的类, 表示一个任务, 这个任务就需要包含两方面: 任务的内容, 任务的实际执行时间.

    实际执行时间可以用时间戳表示, 在调用schedule()的时候, 先获取到当前的系统时间, 在这个基础上, 加上delay时间间隔, 得到了真实要执行这个任务的时间

  2. 使用一定的数据结构, 把多个TimerTask组织起来.

    • 如果使用List组织TimerTask的话, 如何确定此时该执行那个任务呢? 如果使用一个线程不停的对List进行遍历, 查看该任务是否到达执行时间, 就会很低效.

    • 优化: 不需要扫描所有的任务, 我们只需要关注时间最靠前的任务就行, 因为时间最早的任务还没有执行的话, 其他的任务时间也肯定还没有到. 那么如何知道那个任务时间是最靠前的呢? 此时我们就想到了一种数据结构叫做堆/优先级队列.

    • 所以使用 堆/优先级队列 来组织这些任务, 就能以最快的速度找到时间最靠前的, 也就是队首元素.

    • 针对一个任务的扫描也不必反复的一直执行, 而是在获取到队首元素的时间后, 和当前的系统时间做个差值, 根据这个差值来觉该线程休眠等待的时间, 在到达这个时间之前, 不会进行重复扫描. 这样便大大降低了扫描的次数. 并且提高了资源的利用率, 避免了不必要的cpu浪费.

  3. 提供一个扫描线程: 一方面 负责监控队首元素是否到达执行时间; 另一方面 当任务到达执行时间后, 调用run()执行任务.

  4. 注意线程安全问题: 主线程和扫描线程都会对队列操作, 要注意线程安全, 需要加锁.

//创建一个类, 描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
    //任务的具体执行时间
    private long time;
    //具体任务
    private Runnable runnable;
    /**
     * @param runnable 具体执行的任务
     * @param delay 是一个相对时间差
     */
    public MyTimerTask (Runnable runnable, long delay) {
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }
    public long getTime() {
        return time;
    }
    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}
//定时器类
class MyTimer {
    //锁对象
    private Object object = new Object();
    //使用优先级队列保存任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    /**
     * 定时器的核心方法, 就是把要执行的任务添加到队列中
     * @param runnable 要执行的任务
     * @param delay 相对时间差
     * 主线程操作queue, 需要加锁
     */
    public void schedule(Runnable runnable, long delay) {
        synchronized (object) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            //每次添加新的任务后之前休眠的线程唤醒, 根据最新的任务情况, 更新最新任务的执行时间和线程休眠时间
            object.notify();
        }
    }
    /**
     * 扫描线程: 
     *      一方面 负责监控队首元素是否到达执行时间,
     *      另一方面 当任务到达执行时间后, 调用run()执行任务.
     * 对queue的操作需要加锁
     */
    public MyTimer() {
        //扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (object) {
                        //只要queue是空, 那么线程开始休眠, 直到队列不再为空
                        while (queue.isEmpty()) {
                            object.wait();
                        }
                        //获取队首元素
                        MyTimerTask myTimerTask = queue.peek();
                        //或许当前时间
                        long curTime = System.currentTimeMillis();
                        //将当前时间与任务执行时间做比较, 判断是否执行
                        if (curTime >= myTimerTask.getTime()) {
                            queue.poll();
                            //时间到了, 直接执行
                            myTimerTask.getRunnable().run();
                        } else {
                            //时间不到, 当前线程休眠
                            object.wait(myTimerTask.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //线程开始
        t.start();
    }
}

object.wait(myTimerTask.getTime() - curTime);

object.wait();

这里为什么不用sleep()?

  1. 因为sleep()休眠的线程不会释放锁, 那么如果扫描线程正在休眠的话, 主线程调用schedule就会出现阻塞. 也不能因为线程在休眠而不能添加新的任务了吧~
  2. sleep在休眠的过程中, 不方便被提前唤醒. 因为每次添加新的任务都要把之前休眠的线程唤醒, 并且根据最新的任务情况, 更新最新任务的执行时间.

你可能感兴趣的:(Javaee,java)