定时器 Timer(超详细模拟实现)

目录

一、定时器

1.定时器概述

2.Java标准库提供的定时器类

3.定时器代码样例

二、实现

1.实现思路

2.代码实现

2.1纯享版

2.2注释版

3.代码解析(超详细)

3.1描述类MyTimerTask

①构造:MyTimerTask(Runnable runnable,  long delay)

②排序:compareTo(MyTimerTask  o)

③另两个

3.2※定时器类MyTimer

①任务队列入队:schedule(Runnable runnable, long delay)

②扫描线程|构造: MyTimer()

③线程安全问题处理

4.一般执行流程分析


 

5ed82899d812479d89597547c2e1431a.png

一、定时器

1.定时器概述

定时器作为软件开发中的重要组件,用于在特定的时间间隔内执行某些任务。类似于我们生活中的“闹钟”,在关键时刻提醒我们开始工作。当然,程序可不像人类一样有惰性,该出手时就出手,可不会拖拖拉拉,能够阻止它的只有bug或它自己。

它的应用场景,例如,在网络通信中,如果需要在500ms内没有收到数据时断开连接并尝试重新连接,就需要使用定时器。另外,定时器还可以用于定期执行一些维护任务,例如清理缓存、备份数据等。

2.Java标准库提供的定时器类

java.util.Timer是Java提供的定时器标准类,使用它可以在指定时间后执行某个任务。而TimerTask类则是保存这份任务的载体,它存储任务的代码及执行的时间,实现了Runnable接口。

Timer类的核心方法是schedule,有两个参数,一个参数指定即将要执行的任务代码,第二个参数指定多长时间后执行(单位为毫秒)。

方法参数1参数2

public void schedule(TimerTask task, long delay)

将要执行的任务代码

指定多长时间后执行

(单位为毫秒)

        //创建Timer对象
        Timer timer = new Timer();
        //设置三秒后输出"Hello timer!!"
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Hello timer!!");
            }
        }, 3000);

Timer对象实例化(new)后需要手动停止,否则Timer创建的前台线程会持续工作,直到调用cancel方法后结束工作。

方法参数

public void cancel()

f57f6366cda44ad68de69147c82e5ffa.png

3.定时器代码样例

多任务差时测试

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo3 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000ms Hello timer!!");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000ms Hello timer!!");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000ms Hello timer!!");
            }
        }, 1000);
    }
}

55b1196424964a659b4fbfadf6d49408.png

二、模拟实现

1.实现思路

参考标准库中的定时器实现,有如下构成:

  • 优先级队列,按执行时间循序存储任务
  • 一个schedul方法进行入队
  • 一个task类描述任务
  • 一个持续运转的线程,扫描到时间的任务并执行
  • 其他属性和方法,如时间time属性,构造方法

就像是一个业务窗口(扫描线程),预约的人(任务)排成队伍(优先级队列),随时有其他人根据预约时间(执行时间)先后插入(入队)其中,业务窗口一直面对的都是其中最先预约的人(队首),而且还要等到人预约时间到了才办理相应的业务(执行任务)。

b734a6af0ba248b7a6e0b3e552cf8584.png

根据如上结构我们分别要实现两个类,MyTimer和MyTimerTask。

MyTimer在调用构造方法时就要将扫描线程创建出来,不断判断优先级队列中的队首任务是否到了执行时间,到了就执行任务,若优先级队列为空,则阻塞等待入队方法schedule的调用。

MyTack用于描述任务,需要time属性记录执行时间,runnable对象存储任务(runnable重写run方法),同时,想要成为优先级队列的成员必须是可比较的,因此要实现Comparable接口和compareTo方法,构造方法所需属性由MyTimer类的入队方法schedule提供。

2.代码实现

2.1纯享版

import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask> {
    private long time;
    private Runnable runnable = null;

    //构造方法
    public MyTimerTask(Runnable runnable, long delay) {
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    //执行方法,执行runnable存的任务
    public void run() {
        runnable.run();
    }

    //用于获取任务执行时间
    public long getTime() {
        return this.time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    private Thread thread = null;
    private Object lock = new Object();
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //构造方法
    public MyTimer() {
        thread = new Thread(() -> {
            while(true) {
                try {
                    synchronized(lock) {
                        if(queue.isEmpty()) {
                            lock.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long time = System.currentTimeMillis();
                        while(time < task.getTime()) {
                            lock.wait(task.getTime() - System.currentTimeMillis());
                            time = System.currentTimeMillis();
                            task = queue.peek();
                        }
                        queue.poll();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }

    //入队方法,一个参数指定即将要执行的任务代码,第二个参数指定多长时间后执行(单位为毫秒)
    public void schedule(Runnable runnable, long delay) {
        synchronized(lock) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            lock.notify();
        }
    }
}

2.2注释版

import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask> {
    //执行时间
    private long time;
    //执行的任务
    private Runnable runnable = null;

    //构造方法,参数delay提供的是’相对时间‘,runnable提供的是任务
    public MyTimerTask(Runnable runnable, long delay) {
        //执行时间 = 当前时间 + 绝对时间
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    //执行方法,执行runnable存的任务
    public void run() {
        runnable.run();
    }

    //用于获取任务执行时间
    public long getTime() {
        return this.time;
    }

    //用于比较执行顺序
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    //扫描线程,用于扫描任务队列,执行到点的任务
    private Thread thread = null;
    //锁
    private Object lock = new Object();
    //优先级队列,任务队列,对任务按执行时间进行排序
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //构造方法
    public MyTimer() {
        //构造方法中创建线程,构造后使扫描线程直接开工
        thread = new Thread(() -> {
            //while循环持续扫描
            while(true) {
                try {
                    //加锁,保障线程安全
                    synchronized(lock) {
                        //当队列为空时阻塞线程,入队操作执行后解除阻塞
                        if(queue.isEmpty()) {
                            lock.wait();
                        }
                        //获取队首任务,获取当前时间
                        MyTimerTask task = queue.peek();
                        long time = System.currentTimeMillis();
                        //当前时间不到任务执行时间时阻塞线程,等待
                        while(time < task.getTime()) {
                            //设置极限等待时间,到时间自动解除阻塞
                            lock.wait(task.getTime() - System.currentTimeMillis());
                            //等待期间可能又有任务入队,且成为新的队首
                            //而入队操作会解除阻塞,这时候要重置time和队首数据,再通过循环判断当前是否要执行
                            time = System.currentTimeMillis();
                            task = queue.peek();
                        }
                        //执行任务,并从任务队列取出已执行任务
                        queue.poll();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }

    //入队方法,一个参数指定即将要执行的任务代码,第二个参数指定多长时间后执行(单位为毫秒)
    public void schedule(Runnable runnable, long delay) {
        synchronized(lock) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            //入队
            queue.offer(task);
            //入队时为扫描线程解除阻塞
            lock.notify();
        }
    }
}

3.代码解析(超详细)

3.1描述类MyTimerTask

再看一眼MyTask的要求:MyTack用于描述任务,需要time属性记录执行时间,runnable对象存储任务(runnable重写run方法),同时,想要成为优先级队列的成员必须是可比较的,因此要实现Comparable接口和compareTo方法,构造方法所需属性由MyTimer类的入队方法schedule提供。

一共需要两个变量,long类型time用于记录执行任务的时间,Runnable类对象runnable存储任务。

e8a0a84c80d84613bfc8e3159efd49eb.png

四个方法,构造方法MyTimerTask用于初始化time和runnable,compareTo重写方法用于比较,run方法来执行任务,getTime方法用于外界获取任务执行时间来判断是否执行。

①构造:MyTimerTask(Runnable runnable,  long delay)

没什么好说的,干脆利落的将参数赋给对应的成员变量。

参数方面,考虑到代码实用性,所以传的时相对时间,也就是多少ms后执行任务,因此赋值给time时才要加上当前时间。

    //构造方法,参数delay提供的是’相对时间‘,runnable提供的是任务
    public MyTimerTask(Runnable runnable, long delay) {
        //执行时间 = 当前时间 + 绝对时间
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

②排序:compareTo(MyTimerTask  o)

“return this.time - o.time” 意为时间较time大者调用compareTo返回正值,反之返回赋值,相等则返回0(不明白的话可以复习一下).

这样重写是为了让任务队列队首是time最小,即最先要执行的那个任务。

    //用于比较执行顺序
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }

③另两个

 没必要再提,略……

3.2※定时器类MyTimer

MyTimer在调用构造方法时就要将扫描线程创建出来,不断判断优先级队列中的队首任务是否到了执行时间,到了就执行任务,若优先级队列为空,则阻塞等待入队方法schedule的调用。

MyTimer使用的是优先级队列,且要在全局都访问到,所以要定义成全局变量。锁lock同理。

    //扫描线程,用于扫描任务队列,执行到点的任务
    private Thread thread = null;
    //锁
    private Object lock = new Object();
    //优先级队列,任务队列,对任务按执行时间进行排序
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

剩下就是两个核心方法,构造方法,以及任务队列入队方法。

①任务队列入队:schedule(Runnable runnable, long delay)

先从相对简单多的schedule方法开始。

逻辑很简单,根据参数实例化任务描述类(task),并将其插入任务队列(优先级队列queue)中,优先级队列会将插入的task按照执行时间从近到远排序,方便后续执行任务。最后,若扫描线程(thread)陷入阻塞,即之前队列为空时或还未到任务执行时间时。

注意,插入后队首可能会发生变化,notify操作又会解除阻塞,可能导致误判出现bug。

    //入队方法,一个参数指定即将要执行的任务代码,第二个参数指定多长时间后执行(单位为毫秒)
    public void schedule(Runnable runnable, long delay) {
        synchronized(lock) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            //入队
            queue.offer(task);
            //入队时为扫描线程解除阻塞
            lock.notify();
        }
    }

②扫描线程|构造: MyTimer()

预期效果:不断扫描任务队列,当队首任务可执行时自动执行;不可执行时阻塞等待,到点自动执行;队列为空时线程阻塞等待。要避免“忙等”浪费资源。

首先,thread扫描线程在构造第一时间就要创建,开始“扫描”避免错漏或耽搁,代码逻辑要在线程内部实现。

    //构造方法
    public MyTimer() {
        //构造方法中创建线程,构造后使扫描线程直接开工
        thread = new Thread(() -> {
            //while循环持续扫描
            while(true) {
                try {
                    //加锁,保障线程安全
                    synchronized(lock) {
                        //当队列为空时阻塞线程,入队操作执行后解除阻塞
                        if(queue.isEmpty()) {
                            lock.wait();
                        }
                        //获取队首任务,获取当前时间
                        MyTimerTask task = queue.peek();
                        long time = System.currentTimeMillis();
                        //当前时间不到任务执行时间时阻塞线程,等待
                        while(time < task.getTime()) {
                            //设置极限等待时间,到时间自动解除阻塞
                            lock.wait(task.getTime() - System.currentTimeMillis());
                            //等待期间可能又有任务入队,且成为新的队首
                            //而入队操作会解除阻塞,这时候要重置time和队首数据,再通过循环判断当前是否要执行
                            time = System.currentTimeMillis();
                            task = queue.peek();
                        }
                        //执行任务,并从任务队列取出已执行任务
                        queue.poll();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }

因为代码过长且不美观,我们将thread内每次循环的代码逻辑抠出来分析:

    synchronized(lock) {
        //1.当队列为空时阻塞线程,入队操作执行后解除阻塞
        if(queue.isEmpty()) {
        lock.wait();
        }
        //2.获取队首任务,获取当前时间
        MyTimerTask task = queue.peek();
        long time = System.currentTimeMillis();
        //3.当前时间不到任务执行时间时阻塞线程,等待
        while(time < task.getTime()) {
        //设置极限等待时间,到时间自动解除阻塞
        lock.wait(task.getTime() - System.currentTimeMillis());
        //等待期间可能又有任务入队,且成为新的队首
        //而入队操作会解除阻塞,这时候要重置time和队首数据,再通过循环判断当前是否要执行
        time = System.currentTimeMillis();
        task = queue.peek();
        }
        //4.执行任务,并从任务队列取出已执行任务
        queue.poll();
        task.run();
    }
  1. 首先,判断队列是否为空,为空则阻塞。(为空说明当前无任务要执行,阻塞释放锁,一方面可以节省系统资源,另一方面不释放锁没法进行入队操作添加任务,进而永久阻塞)

  2. 获取队首和当前时间,用变量单独存储。(一方面减少代码冗余,另一方面为下面while循环判断做准备)
  3. while循环判断当前队首任务是否可以执行,不可以则阻塞。(直接使用wait()阻塞不好使用notify解锁,但加上自动解除阻塞时间后就不需要在考虑额外notify的问题了。还有,重置time和task是为了避免阻塞结束后队首发生变化,而当前时间是一定变的。)
  4. 到达执行时间,通过task调用run()执行任务,并把执行过的任务从队列中剔除掉。(这个任务一定是首先要被执行的那个,队首最小。)

外层while循环不断循环上述逻辑就达到了我们所有的预期效果,不断扫描,执行可执行任务。线程安全问题解决具体请往下看。

③线程安全问题处理

MyTimer类的实现是可能出现一些线程安全问题的,那又是如何将这些问题解决的呢。

大部常规问题解决:

读写操作一同出现时很容易出现线程安全问题,上述MiTimer的代码实现中两个方法都同时涉及到了读写操作,如schedule方法中涉及到了queue,而PriorityQueue类型并非是线程安全的。

解决方法也很简单将两个方法涉及到queue的部分都用synchronized包裹起来。

schedule方法调用导致队首变化问题解决:

当MyTimer()代码逻辑执行到“时间不到任务执行时间时阻塞线程”时线程阻塞,在等待期间可能schedule方法会被多次调用,其中可能会出现改变queue队首的情况。

配合schedule中notify的解除阻塞操作,在判断条件是“time < task.getTime()”的情况下,如果task和time(尤其是task)不发生变化就会导致循环判断的其实不是队首这个应该最先执行的任务。

        //当前时间不到任务执行时间时阻塞线程,等待
        while(time < task.getTime()) {
        //设置极限等待时间,到时间自动解除阻塞
        lock.wait(task.getTime() - System.currentTimeMillis());
        //等待期间可能又有任务入队,且成为新的队首
        //而入队操作会解除阻塞,这时候要重置time和队首数据,再通过循环判断当前是否要执行
        time = System.currentTimeMillis();
        task = queue.peek();
        }

在阻塞后重置task和time就很好的解决了这一安全问题。

4.一般执行流程实例

public class ThreadDemo {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000ms Hello timer!!");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000ms Hello timer!!");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000ms Hello timer!!");
            }
        }, 1000);
    }
}

实例化MyTimer,扫描线程启动。

MyTimer timer = new MyTimer();

调用schedule方法添加任务,实例化对应的任务对象并插入,优先级队列自动排序如下。

5161fd549c6d4c36af5334b9b85e830e.png

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000ms Hello timer!!");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000ms Hello timer!!");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000ms Hello timer!!");
            }
        }, 1000);

扫描线程不间断扫描,开始时(实列化MyTimer时)由于队列为空而阻塞,入队添加任务后解除阻塞。

判断队首是否需要执行,由于1000ms后执行,所以暂时阻塞至一秒后自动解除阻塞。

判断通过出队,执行任务,打印"1000ms Hello timer!!"

同理,1000ms后,打印"2000ms Hello timer!!"

又1000ms后,打印"3000ms Hello timer!!"

    public MyTimer() {
        thread = new Thread(() -> {
            while(true) {
                try {
                    synchronized(lock) {
                        if(queue.isEmpty()) {
                            lock.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long time = System.currentTimeMillis();
                        while(time < task.getTime()) {
                            lock.wait(task.getTime() - System.currentTimeMillis());
                            time = System.currentTimeMillis();
                            task = queue.peek();
                        }
                        queue.poll();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }

任务队列清空,扫描线程阻塞,等待下次调用schedule方法解除阻塞……

55b1196424964a659b4fbfadf6d49408.png

博主是Java新人,每位同志的支持都会给博主莫大的动力,如果有任何疑问,或者发现了任何错误,都欢迎大家在评论区交流“ψ(`∇´)ψ

0043e0ffc2fa494c8ba6ea472ed59f70.png

 

  • 30
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值