• 【JavaEE初阶】 定时器详解与实现


    🌴定时器是什么

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

    定时器是一种实际开发中非常常用的组件.
    比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
    比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
    类似于这样的场景就需要用到定时器.

    🎋Java标准库中的定时器

    • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .

    • schedule 包含两个参数.

    • 第一个参数指定即将要执行的任务代码,

    • 第二个参数指定多长时间之后执行 (单位为毫秒)

    代码示例:

    下面程序分别有一个定时器,设置了三个不同的时间

    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TestDemo {
        public static void main(String[] args) {
            Timer timer = new Timer();
            System.out.println("程序启动!");
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("定时器3");
                }
            },3000);
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("定时器2");
                }
            },2000);
            timer.schedule(new TimerTask() {
                @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
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    运行结果如下:
    在这里插入图片描述
    结果如我们所示,按照时间顺序进行打印

    🌲模拟实现定时器

    首先我们先来看一下定时器的构成

    🚩定时器的构成

    1. 一个带优先级的阻塞队列
    • 为啥要带优先级呢?
      因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.
    1. 队列中的每个元素是一个 MyTask 对象.

    2. MyTask 中带有一个时间属性, 队首元素就是即将要执行的任务

    3. 同时有一个线程一直扫描队首元素, 看队首元素是否需要执行

    📌第一步:MyStack类的建立

    包含两个属性

    • 包含一个 Runnable 对象

    • 一个 time(毫秒时间戳)

    由于我们的MyTask类需要放入一个带优先级的阻塞队列中,所以我们需要MyTack可以比较,这里博主选择重写 Comparable 接口里的compareTo方法

    代码实现如下:

    public class MyTask implements Comparable<MyTask> {
        private Runnable runnable;
        private  long time;
    
        public MyTask() {
            System.out.println(1);
        }
        public void tad() {
            System.out.println(2);
        }
        public MyTask(Runnable runnable, long time) {
            this.runnable = runnable;
            this.time = time;
        }
        public long gettime(MyTask) {
            return this.time;
        }
        //执行任务
        public void run() {
            runnable.run();
        }
        @Override
        public int compareTo(MyTask o) {
            return (int)(this.time - o.time);
        }
    }
    
    • 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

    📌第二步:创建MyTimer类

    该类需要有一个带有优先级的阻塞队列

    还需要有一个可schedule 方法用于我们来插入我们我们需要执行的任务

    public class MyTimer {
        // 有一个阻塞优先级队列, 来保存任务.
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    
        // 指定两个参数
        // 第一个参数是 任务 内容
        // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
        public void schedule(Runnable runnable, long after) {
            // 注意这里的时间上的换算
            MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
            queue.put(task);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 由于我们输入的都是一个时间大小,所以我们需要进行处理一下,

    • 这里的System.currentTimeMillis()是获取当时的时间戳

    • 再加上所需要的时间大小即达到我们效果

    其次还需要一个线程循环扫描

    • 该扫描我们需要做的是

    • 取出队首元素, 检查看看队首元素任务是否到时间了.

    • 如果时间没到, 就把任务塞回队列里去.

    • 如果时间到了, 就把任务进行执行.

        private Thread t = null;
        public MyTimer() {
            t = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            // 取出队首元素, 检查看看队首元素任务是否到时间了.
                            // 如果时间没到, 就把任务塞回队列里去.
                            // 如果时间到了, 就把任务进行执行.
                            MyTask myTask = queue.take();
                            long curTime = System.currentTimeMillis();
                            if (curTime < myTask.getTime()) {
                                // 还没到点, 先不必执行
                                // 现在是 13:00, 取出来的任务是 14:00 执行
                                //塞回去
                                queue.put(myTask);
                            } else {
                                // 时间到了!! 执行任务!!
                                myTask.run();
                            }
                        } 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

    📌第三步:解决相关问题

    • 问题一:while (true) 转的太快了, 造成了无意义的 CPU 浪费.

    比如第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队首元素几万次.

    解决办法:引入一个locker对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题.

    我们在循环扫描里:引入 wait, 等待一定的时间.并修改 MyTimer 的 schedule 方法, 每次有新任务到来的时候唤醒一下循环扫描线程. (因为新插入的任务可能是需要马上执行的)

    • 问题二:原子性问题

    由于我们的出队列操作和判断语句不具有原子性

    问题情况如下:

    出队列操作拿到任务后,还没有进行判断

    然后这时候有一个来了一个新任务
    在这里插入图片描述
    但是此时我们该任务还没有wait()操作,而且我们由于添加新元素,notify()操作已执行,这就导致后面的wait操作不会被唤醒,那么新来的任务就在相应时间来没有被执行

    解决方法:将出队列操作与判断操作都加上锁

    代码实现如下:

    import java.util.concurrent.PriorityBlockingQueue;
    
    public class MyTimer {
        // 有一个阻塞优先级队列, 来保存任务.
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        // 扫描线程
        private Thread t = null;
        private Object locker = new Object();
        public MyTimer() {
            t = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            // 取出队首元素, 检查看看队首元素任务是否到时间了.
                            // 如果时间没到, 就把任务塞回队列里去.
                            // 如果时间到了, 就把任务进行执行.
                            synchronized (locker) {
                                MyTask myTask = queue.take();
                                long curTime = System.currentTimeMillis();
                                if (curTime < myTask.getTime()) {
                                    // 还没到点, 先不必执行
                                    // 现在是 13:00, 取出来的任务是 14:00 执行
                                    queue.put(myTask);
                                    // 在 put 之后, 进行一个 wait
                                    locker.wait(myTask.getTime() - curTime);
                                } else {
                                    // 时间到了!! 执行任务!!
                                    myTask.run();
                                }
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            t.start();
        }
        // 指定两个参数
        // 第一个参数是 任务 内容
        // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
        public void schedule(Runnable runnable, long after) {
            // 注意这里的时间上的换算
            MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
            queue.put(task);
            synchronized (locker) {
                locker.notify();
            }
        }
    }
    
    • 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

    🌳完整代码实现与测试

    计时器完整代码:

    import java.util.concurrent.PriorityBlockingQueue;
    
    class MyTask implements Comparable<MyTask> {
        private Runnable runnable;
        private  long time;
    
        public MyTask() {
            System.out.println(1);
        }
        public void tad() {
            System.out.println(2);
        }
        public MyTask(Runnable runnable, long time) {
            this.runnable = runnable;
            this.time = time;
        }
        public long getTime() {
            return this.time;
        }
        //执行任务
        public void run() {
            runnable.run();
        }
        @Override
        public int compareTo(MyTask o) {
            return (int)(this.time - o.time);
        }
    }
    
    public class MyTimer {
        // 有一个阻塞优先级队列, 来保存任务.
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        // 扫描线程
        private Thread t = null;
        private Object locker = new Object();
        public MyTimer() {
            t = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            // 取出队首元素, 检查看看队首元素任务是否到时间了.
                            // 如果时间没到, 就把任务塞回队列里去.
                            // 如果时间到了, 就把任务进行执行.
                            synchronized (locker) {
                                MyTask myTask = queue.take();
                                long curTime = System.currentTimeMillis();
                                if (curTime < myTask.getTime()) {
                                    // 还没到点, 先不必执行
                                    // 现在是 13:00, 取出来的任务是 14:00 执行
                                    queue.put(myTask);
                                    // 在 put 之后, 进行一个 wait
                                    locker.wait(myTask.getTime() - curTime);
                                } else {
                                    // 时间到了!! 执行任务!!
                                    myTask.run();
                                }
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            t.start();
        }
        // 指定两个参数
        // 第一个参数是 任务 内容
        // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
        public void schedule(Runnable runnable, long after) {
            // 注意这里的时间上的换算
            MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
            queue.put(task);
            synchronized (locker) {
                locker.notify();
            }
        }
    }
    
    • 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

    测试代码如下

    public class TestDemo2 {
        public static void main(String[] args) {
            MyTimer myTimer = new MyTimer();
            System.out.println("程序启动");
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("计时器3");
                }
            },3000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("计时器2");
                }
            },2000);
            myTimer.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
    • 20
    • 21
    • 22
    • 23
    • 24

    测试结果如下:
    在这里插入图片描述

    ⭕总结

    关于《【JavaEE初阶】 定时器详解与实现》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

  • 相关阅读:
    安卓虚拟机点击按钮后为什么报错
    filter的理解是使用(日常复习)
    十分钟入门以太和Opensea测试网批量发行NFT实战
    前端&UI核心
    MFC工控项目实例一主菜单制作
    Day6:无序二叉树的插入以及遍历(前/中/后序)
    iOS17正式版BUG汇总:无法正常拨打电话、小组件不可用、无线充电不可用等问题
    k8s--基础--26.2--监控告警系统--prometheus--node-exporter
    laravel 安装后台管理系统, filament.
    某Flutter-APP逆向分析
  • 原文地址:https://blog.csdn.net/m0_71731682/article/details/133957196