• 定时器的简单使用和实现


    什么是定时器

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

    标准库中的定时器

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

    使用

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

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

    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("3000");
        }
    }, 3000);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种写法很像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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    步骤:

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

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

    object.wait();

    这里为什么不用sleep()?

    1. 因为sleep()休眠的线程不会释放锁, 那么如果扫描线程正在休眠的话, 主线程调用schedule就会出现阻塞. 也不能因为线程在休眠而不能添加新的任务了吧~
    2. sleep在休眠的过程中, 不方便被提前唤醒. 因为每次添加新的任务都要把之前休眠的线程唤醒, 并且根据最新的任务情况, 更新最新任务的执行时间.
  • 相关阅读:
    spring MVC
    如何将CAD导入GIS并且找到正确的投影坐标
    艾美捷ICT FLICA天冬氨酸蛋白酶(Caspase)活性检测试剂盒说明书
    Softing IT Networks线上研讨会 | 9月 (上篇)
    【RV1103】如何新增一个新板级配置
    第四篇 本地开发环境搭建
    多肽标签TC tag,H2N-CCPGCC-OH
    关于qt中label挡住了dockwidget的窗体边缘
    Python中的标签边缘颜色
    Hexo+Github 快速搭建个人博客
  • 原文地址:https://blog.csdn.net/m0_73594607/article/details/134537210