8/20 Java学习博客

新的日常开发组件:定时器

作用:约定一个时间,时间到达之后,执行某个代码逻辑

定时器非常常见尤其实进行网络通信的时候

就比如客户端发出请求就要等待响应,但是服务器迟迟没有响应,因为网络环境的复杂,也不知道请求是没发过去,还是响应丢了,或是服务器出问题了,对于客户端来说,不能无限的等,需要有一个最大的期限,到达这个期限后,是重新发一遍还是放弃还是其他什么。

这是这个等待的最大期限,就是通过定时器方式来实现

首先要使用定时器 其实这个使用方法跟创建线程时候差不多吧我感觉

先写一个类继承TimerTask类 然后重写run方法

class TimerTask1 extends TimerTask{
    @Override
    public void run() {
        System.out.println("执行定时器的任务1000");

    }
}

这可能是因为TimerTask 他也实现了Runnable接口


这里我就用TimerTask1来继承的   
//创建TimeTask1实例  
TimerTask1 timerTask1 = new TimerTask1();

//然后创建Timer实例
Timer timer = new Timer();

//给定时器安排任务 不是像线程一样
/* Runnable runnable = new MyRunnable();
        Thread t =new Thread(runnable);
        t.start();
*/

//他是需要这个schedule 来安排一个任务,预定时间去执行

timer.schedule(timerTask1,1000);  //当前安排的任务啥时候执行,以当前时刻为基准往后推迟xxx ms

这样一个定时器的使用就写好了

当然对于这个schedule也是可以使用匿名内部类的写法 继承TimerTask创建实例,重写run方法

timer.schedule(new TimerTask(){//此处使用匿名内部类的写法,继承了TimerTask并且创造出一个实例
            @Override
            public void run(){//目的重写run方法 通过run描述任务的详细情况
                System.out.println("执行定时器的任务2000");

            }
        },2000);

主线程执行schedule方法时候,就是把任务放到timer对象中了,与此同时,timer里面也包含了一个线程,叫做扫描线程,一旦时间到了,扫描线程就会执行刚才安排的任务了

仔细观察可以发现,全部打印完后,进程依然没有结束,就是因为timer内部线程阻止了进程结束。

我们不光要学怎么用 还要学怎么实现简单定时器

首先分析下定时器中所需

1.需要有一个线程,来扫描任务是否到时间,可以执行了。
2.需要有一个数据结构,把所有的任务都保存起来。
3.还需要创建个,通过类的对象来描述一个任务(至少包含任务内容和时间)

所以我们先创建类 通过这个类描述对象,然后在里面定义一些必要字段,然后写一个构造方法

class MyTimerTask{


     private long time;//执行时间的时间
     private Runnable runnable;

     public MyTimerTask(Runnable runnable,long delay){
         this.runnable=runnable;
         this.time=System.currentTimeMillis()+delay;
     }
//此处的delay是相对时间,其实就是我们在util里面的timer.schedule(timertask,delay)里设置的多少ms后执行的delay

     public long getTime(){
         return time;
     }
     public Runnable getRunnable(){
         return runnable;
     }

}

一个初步的类就搞好了

接着我们实现下保存对象的数据结构,那优先级队列最好了,因为我们需要每次等待都是等待最新的任务。

接着这里我们写定时器里面还包含了扫描线程

扫描线程实现这3个主要地方:

  1. 队列为空时,阻塞等待新任务加入
    2.队列不为空,查看队首任务
    3.时间到了,弹出并执行任务,时间没到进行等待

然后我们就要队列空实现阻塞 加锁 有阻塞还要有唤醒
所以我们在添加任务的schedule方法上用lock.notify()

队列不为空 则就先查看队首任务

定时器主体应该这样写

//自己的定时器
class MyTimer{
    //使用一个数据结构 优先级队列,保存所有要安排的任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<MyTimerTask>();

    //使用这个对象作为锁对象
    private Object lock = new Object();

    //通过schedule方法把要完成的任务,构造成个任务对象放入 队列里面

    //对于优先级队列,要求里面的元素务必可比较。 还有TreeSet TreeMap
    public void schedule(Runnable runnable,long delay){
        synchronized (lock){
            queue.offer(new MyTimerTask(runnable,delay));
            lock.notify();
        }

    }

    //搞个扫描线程
    public MyTimer(){
        //创建一个扫描线程
        Thread t = new Thread(()->{
            while (true) {
                try {
                    synchronized (lock) {
                        // 1. 队列为空时,阻塞等待新任务加入
                        while (queue.isEmpty()) {
                            lock.wait();
                        }

                        // 2. 队列不为空,查看队首任务
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();

                        if (curTime >= task.getTime()) {
                            // 时间到了,弹出并执行任务
                            task = queue.poll();
                            task.getRunnable().run();
                        } else {
                            // 时间还没到,有限期等待对应的差值时间
                            // 在此期间如果有更新的时间更早的任务被 schedule 入队,notify() 会提前唤醒它
                            lock.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
 /*    这里回忆下 初次开始的错误写法
            逻辑结构严重错位(核心卡死原因)

现象:当队列为空时,线程执行到 lock.wait() 进入阻塞。即使此时主线程通过 schedule() 插入了任务并调用 lock.notify() 唤醒它,线程唤醒后会继续从 wait() 下一行执行。

问题:在我最初的代码里,queue.peek() 和执行任务的逻辑全写在 catch 代码块之前(紧接着 wait() 后面)。一旦通过 lock.wait() 挂起,它只会在被唤醒的那一次去执行 peek()。后续如果不继续手动 notify,或者队列不为空时,它甚至根本进不去 while (queue.isEmpty()) 循环,导致后续任务完全无法处理!

            //需要不停扫描队首元素 是否到达时间
            while(true){
                synchronized (lock){
                    //取队首元素,前提是队列是否为空,空的话就是阻塞等待
                    while(queue.isEmpty()){
                        //使用wait进行等待,但是要需要搭配synchronize加锁
                        try {
                            lock.wait();
                            //这里的wait()很明显需要由添加新的任务的线程唤醒
                            MyTimerTask task = queue.peek();

                            long curTime = System.currentTimeMillis();
                            if(curTime >= task.getTime()){//当前时间到达任务时间,可以执行任务
                                task.getRunnable().run();
                                //任务执行完了
                                queue.poll();

                            }else{
                                //时间没到,暂时啥都不干,等待下一轮判定
                            }

                            //比较下当前队首元素是否可以执行了

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }

        });*/

        t.start();
    }
}


说到队首任务 就不得不提到我们这个 优先级队列

PriorityQueue 会认为:
你没给我 Comparator,那我就把元素当成 Comparable 来比较。

但你的对象又不能比较。
所以你有两种写法都可以。
第一种,重写 compareTo
第二种,不让 MyTimerTask 实现 Comparable,直接创建队列时告诉它怎么排

这里我们选择第一种 实现这个接口并且重写 使得 比较后是队首元素最小即时间最短

因为用到了比较时间所以我们还要在类对象加一个获取时间 getTime
然后我们这里的查看对象是用MyTimerTask task = queue.peek();
获取任务然后执行,还需要getRunnable;

所以 类应该这么写

class MyTimerTask implements Comparable<MyTimerTask>{

    //要有一个执行的任务
    private Runnable runnable;
    //还要一个执行任务得时间
    private long time;
    //此处得delay就是schedule方法传入的相对时间
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    @Override
    public int compareTo(MyTimerTask o) {
        //这样的写法,就是让队首元素最小的时间的值
        return (int)(this.time - o.time);
        //如果是想让队首元素是时间最大的
        //return o.time - this.time;
    }
    /**
     * compareTo 返回值约定:
     * 返回负数:this 对象排在前面
     * 返回0:两个对象相等
     * 返回正数:o 对象排在前面
     * PriorityQueue 默认是小顶堆,堆顶(队首)放优先级最小的元素。
     */
    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

}

最后启动扫描线程start()

这样一个简单的计时器就做好了

我其实写的时候一直在想这个我们实现的计时器为什么schedule是传runnable而不是MyTimerTask

官方 Timer:
外部创建 TimerTask

我的 MyTimer:
内部创建 MyTimerTask

其实我们可以

只需要把 schedule 从:

public void schedule(Runnable runnable, long delay) {
    synchronized (lock) {
        queue.offer(new MyTimerTask(runnable, delay));
        lock.notify();
    }
}

改成接收自己的 MyTimerTask:

public void schedule(MyTimerTask task) {
    synchronized (lock) {
        queue.offer(task);
        lock.notify();
    }
}

然后外部创建:

MyTimerTask task = new MyTimerTask(
        () -> System.out.println("执行任务"),
        3000
);

timer.schedule(task);

流程就变成:
外部 new MyTimerTask

timer.schedule(task)

放进 PriorityQueue

到时间执行

不过还是有点不太像吧,毕竟官方的是把delay放在task外面的

总而言之一个简单的计时器就写好了

posted @ 2026-08-30 18:09  曾章吉的浩吧  阅读(3)  评论(0)    收藏  举报