Analog implementation timer

For the use of timers in the java standard library, you can see the use of timers Timer

general idea

        Define a MyTimeTask class, which is used to organize the content of the task to be executed and the time stamp of the task to be executed. Later, it will be compared according to the current system time and the time stamp of the executed task to determine whether to execute the task or wait for the task

        Use a MyTimer class as the main body of the timer, and use the data structure of the small root heap in the MyTimer class to store the tasks to be executed

        Define a schedule method in the MyTimer class, instantiate a task through the parameters passed in, and then insert the task into the small root heap

        Create a scanning thread in the MyTimer class. On the one hand, monitor whether the first element of the heap has reached the point. On the other hand, after the point is reached, the run method in Runable should be called to perform the task, because the task should be started when the MyTimer object is created. scan, so the scan thread should be defined in the constructor of the MyTimer class

Why use a small root heap to store tasks to be executed?

        Because the top of the small root pile is the task with the shortest waiting time, among many tasks, we only need to pay attention to the earliest task to be performed. If the earliest task cannot be executed, other tasks must not be executed. , if we don’t use the small root heap, if we use the linked list, if we want to know the next task to be executed, we need to traverse the tasks in the linked list continuously, compare the timestamp of the executed task with the current time, and keep going It is unreasonable that the traversal representatives have been occupying cpu resources. So it is more reasonable to use the small root heap to store the tasks to be executed

Creation of the MyTimeTask class

        code display

//描述要执行任务的内容,以及执行任务的时间戳
class MyTimeTask implements Comparable<MyTimeTask>{
    //由于该类的对象要存放到小根堆中,所以要实现Comparable接口,重写compareTo方法
    // 才可以给出合适的比较规则,才可以放入堆中
    @Override
    public int compareTo(MyTimeTask o) {
        return (int)(this.time-o.time);
    }
    //要执行任务的内容
    private Runnable runnable;
    //执行任务的时间戳
    private long time;

    //构造函数传入要执行的任务以及等待的时间
    //delay是一个时间差,类似于3000这样的数值
    public MyTimeTask(Runnable runnable,long delay){
        this.runnable=runnable;
        time=System.currentTimeMillis()+delay;  //当前系统时间加上延迟的时间就是任务要执行的时间戳
    }

    public Runnable getRunnable(){
        return runnable;
    }

    public long getTime(){
        return time;
    }
}

        Precautions

        1. Since the time member attribute in the MyTimeTask class represents the timestamp of task execution, but the parameter delay entered by the user is the delay time of task execution, the timestamp of task execution is the current system time plus the delayed time

        2. Since the MyTimeTask class needs to be added to the small root heap, the priority relationship in the heap must be defined in the MyTimeTask class first. Here, the MyTimeTask class is used to implement the Comparable interface, and the compareTo method is rewritten to specify the MyTimeTask class. priority relationship

Creation of the MyTimer class

        code display

/定时器类的主体
class MyTimer{
    //用小根堆这个数据结构来存储要执行的任务
    //因为小根堆堆顶的数是延迟时间最短,最快要执行的任务
    PriorityQueue<MyTimeTask> queue=new PriorityQueue<>();
    Object locker=new Object(); //用来加锁的锁对象

    //定时器的核心方法,将任务添加到堆中
    //涉及到多线程,可能有多个线程调用schedule添加任务到堆中,而且MyTimer内部的线程也要对堆进行修改
    //所以存在线程安全问题,并且schedule和扫描线程应该对同一个对象进行加锁
    public void schedule(Runnable runnable,long delay){
        synchronized(locker){
            MyTimeTask task=new MyTimeTask(runnable,delay);
            queue.offer(task);
            locker.notify();
        }
    }

    //定义一个扫描线程,一方面监控堆首元素是否到点了,一方面在到点后,要调用Runable中的run方法来执行任务
    //扫描线程不应该是用户自己调用的,而是一创建MyTimer对象就要有扫描线程来判断是否有需要执行的任务,所以应该写在构造方法中
    public MyTimer(){
        Thread t=new Thread(()->{
            synchronized(locker){   //因为接下来的代码中都涉及到对堆的操作,所以直接整体加上锁
                while (true){
                    //wait推荐和while搭配使用,因为wait可能是不是被notify正常唤醒的,所以被唤醒了以后条件可能依然不满足
                    // 所以要用while循环对条件是否满足进行多次判断
                    while (queue.isEmpty()){
                        //continue;
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    //取出任务判断是否需要执行,不删除
                    MyTimeTask task=queue.peek();
                    if(task.getTime()<=System.currentTimeMillis()){
                        task.getRunnable().run();   //时间到了要执行任务
                        queue.poll();   //执行任务后将任务从堆中弹出
                    }else {
                        //任务时间还没有到不需要执行,进行睡眠,等时间到了再执行
                        try {
                            //sleep不适合在这里用于进行休眠
                            //1.sleep休眠不会释放锁,就代表在进行休眠的时候虽然线程没有对堆进行操作
                            // 但locker对象还是被锁着的,此时要往队列中添加数据也要进行等待,这是不合理的
                            //2.如果我们新增了一个任务,可能这条任务的执行时间比当前堆顶最近的执行时间还要早进行
                            //此时我们就需要从睡眠状态脱离,去检查是否要更改随眠时间,但sleep睡眠的过程中不方便提前中断
                            //虽然可以用interrupt提前中断,但是使用interrupt意味着程序已经要结束了
                            //所以使用wait来进行休眠是比sleep更加合理的
                            //Thread.sleep(task.getTime()-System.currentTimeMillis());
                            locker.wait(task.getTime()-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });

        //线程定义完毕,启动线程
        t.start();
    }

}

        Precautions

        1. Define a schedule method. The user uses this method by passing in the task execution content and delay time. In the schedule, a MyTimeTask task is instantiated through the parameters passed in by the user, and the task is added to the small root heap.

        2. If you want to perform tasks, you must execute them in the scanning thread inside MyTimer. Since the MyTimer class needs to be instantiated to determine whether there are new tasks to be processed, the scanning thread should be created in the construction method of the MyTimer class and scan through cycles The task at the top of the heap, when the heap is empty, enters the wait sleep until the user calls the schedule method to transfer the task to the heap, then calls notify to release the sleep. About wait and notify, you can see that the order of thread execution is coordinated through wait and notify

        When the heap is not empty, take out the task on the top of the heap to judge whether it needs to be executed according to the timestamp of the task execution and the current system time, and do not delete it. If the current system time and >= task time, you need to call task. getRunnable().run() to execute the task, if the current system time and < task time need to wait for sleep, the sleep time is the timestamp of task execution - the current system time, because the currently scanned task is the earliest Executed, so there is no need for other tasks to scan, they are executed more

3. Why do you need to add synchronized locks         to the schedule method and scanning threads (you can see thread safety issues about synchronized locks ), because the schedule method involves adding data to the heap. If multiple threads call the schedule method, there will be thread safety issues. The scan thread also involves the use of the heap many times, so it also has thread safety issues, so a synchronized lock must be added to ensure thread safety, and because the schedule method and the scan thread both modify the heap, they must The same object is locked.

        4.sleep can also sleep with 1, why do we use wait, notify

                (1).sleep will not release the lock, which means that although the thread does not operate the heap during sleep, the locker object is still locked. At this time, adding data to the queue also requires waiting. This is unreasonable

                (2). If we add a new task, the execution time of this task may be earlier than the latest execution time of the current top of the heap. At this time, we need to leave the sleep state to check whether to change the sleep time , but it is inconvenient to interrupt in advance during the sleep sleep process. Although interrupt can be used to interrupt in advance, using interrupt means that the program is about to end

        So using wait to sleep is more reasonable than sleep

        

Guess you like

Origin blog.csdn.net/q322359/article/details/132007998