• Java EE——定时器


    定时器

    和我们现实中的定时器用途类似,代码中的定时器是设定一个时间,在过去这个时间后,执行某个特定的代码

    例如我们的服务器和客户端传递信息,客户端需要等待服务器的信息,如果超出了一定时间还没有收到消息,那么客户端就应该提醒服务器让他重新发一下消息,这里就可以用到计时器

    标准库中的定时器

    在java.util.Timer包中实现了定时器

    首先实现一个Timer对象

    Timer timer = new Timer();
    
    • 1

    然后为计时器布置任务,第一个参数是一个Runnable对象,重写run方法,就可以让定时器到点后执行其中的代码,第二个参数是微秒,

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

    我们一个Timer对象,可以安排多个schedule任务

    而当我们运行这个代码时,发现程序并没有在执行完代码后退出,这是因为Timer中存在线程来完成任务,之前讲过线程分为前台线程和后台线程(isDaemon判断)而我们的前台线程不会让进程退出

    MyTimer

    (最后有完整代码,前面的代码只是用来讲解而拆分的)

    MyTask

    首先我们实现MyTask,代表计时器中的任务对象

    class MyTask implements Comparable<MyTask>{
        private Runnable runnable;
        private long time;
        MyTask(Runnable runnable, long delay){
            this.runnable = runnable;
            this.time = System.currentTimeMillis() + delay;
        }
    
        public Runnable getRunnable() {
            return runnable;
        }
    
        public long getTime() {
            return 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

    MyTask中有两个参数,一个是Runnable的任务,另一个是时间,时间的大小是现在时刻+传入的时间,我们实现了其构造方法,以及一系列get方法,至于为什么要实现compareTo方法,这个后面会提到

    MyTimer

    接下来实现MyTimer

    class MyTimer{
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        private Object locker = new Object();
        public void schedule(Runnable runnable, long after){
    
            MyTask myTask = new MyTask(runnable, after);
            queue.offer(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.getTime() > curTime){
                                queue.put(myTask);
                                locker.wait(myTask.getTime() - curTime);
                            } else {
                                myTask.run();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            });
            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

    由于我们需要判断哪个任务先执行,哪个任务后执行,并且如果没有任务了就等待任务,任务满了就不让继续放任务,因此我们创建了一个优先级阻塞队列(详细概念在上一篇博客中有提到),这也是为什么MyTask要实现compareTo的原因

    我们在其中添加了schedule方法,在调用这个方法后就可以new一个MyTask任务,并将其放入阻塞队列

    在MyTimer的构造方法中,我们创建了一个线程,其主要功能就是循环判断是否有任务需要执行。先从阻塞队列中取出时间最小的任务,然后将其与现在时间进行比较,如果一样了就执行任务,不一样就放回到队列中。

    为什么要wait和notify

    由于我们不想让线程一直循环执行检测时间的任务,这样太浪费资源了,因此我们思考能不能让线程停一停,首先考虑让线程sleep一个现在时间和任务要执行时间的差值,但是这样有一个问题:如果在这段时间中突然又新增了一个任务,而且这个任务和目前时间的差值比上一个的还小,那么我们就会错过这个任务

    因此,我们不能用sleep,那么综合之前几篇博客所讲的,我们可以用wait和motify:在我们线程扫描的时候,wait(当前时间和任务执行时间差值),当我们添加任务的时候,就notify一下睡着了的线程

    synchronized写法原因

    这时我们还需要考虑synchronized应该圈多大范围的代码块
    如果我们的代码块只圈了wait这一条语句,那么可能会出现如下问题

    当我们的线程a刚从阻塞队列中take出一个任务,这时另一个线程b就调用了put,插入了一个新的任务,并且这个任务的时间比第一个任务的时间还短,然后线程b一直执行到notify语句,线程a才继续执行,这时线程a还是以第一个任务来计算wait时间的,也就是说我们的线程b的notify并没有对线程a起到作用。

    这个问题的出现是因为take和wait计算时间并不是原子性的,从而使线程b有机会插入到其中,因此我们的wait的synchronized代码块应该从take一直圈到wait

    那么我们就想到,是不是只要synchronized代码块圈的越大,代码写的就越对呢,事实上并非如此,我们的notify代码如果和queue.offer()方法被圈到了一起,就会出现死锁问题,这是因为我们的queue是一个阻塞队列,其put的实现也是带有synchronized,但是我们put的synchrozed传入的对象和notify使用的对象是不一样的,这样的话代码就会一直阻塞在put

    因此我们可以发现,线程是非常麻烦而且容易出错的,这也是为什么其他编程语言尝试简化多线程
    例如erlang的actor模型,go的CSP,python的await/async
    但是在java和cpp中,多线程是最基本的编程方式

    全部代码的实现

    import java.util.concurrent.PriorityBlockingQueue;
    
    class MyTimer{
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        private Object locker = new Object();
        public void schedule(Runnable runnable, long after){
    
            MyTask myTask = new MyTask(runnable, after);
            queue.offer(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.getTime() > curTime){
                                queue.put(myTask);
                                locker.wait(myTask.getTime() - curTime);
                            } else {
                                myTask.run();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            });
            t.start();
        }
    
    
    }
    
    class MyTask implements Comparable<MyTask>{
        private Runnable runnable;
        private long time;
        MyTask(Runnable runnable, long delay){
            this.runnable = runnable;
            this.time = System.currentTimeMillis() + delay;
        }
    
        public Runnable getRunnable() {
            return runnable;
        }
    
        public long getTime() {
            return time;
        }
    
        public void run(){
            runnable.run();
        }
    
        @Override
        public int compareTo(MyTask o) {
            return (int) (this.time -o.time);
        }
    }
    
    public class demo {
        public static void main(String[] args) {
            MyTimer myTimer = new MyTimer();
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("时间到");
                }
            },3000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("时间到2");
                }
            },4000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("时间到3a");
                }
            },5000);
        }
    }
    
    
    • 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
    • 86
    • 87
    • 88
    • 89
    • 90
  • 相关阅读:
    猿创征文|瑞吉外卖——移动端_订单明细
    PTA_乙级_1016
    从命令行管理文件
    [附源码]计算机毕业设计JAVA篮球装备商城系统
    表格软件之FineReport-JS实现大数据集导出(二)
    第5讲 使用pytorch实现线性回归
    DolphinScheduler 3.0安装及使用
    基于ASP.NET Core 6.0的整洁架构,asp.net core 6.0 功能
    百度是否收录查询易语言代码
    C#语言进阶(二)—事件 第二篇(.net标准事件模型)
  • 原文地址:https://blog.csdn.net/m0_60867520/article/details/126934910