JaveEE:手动实现定时器精讲

2023-12-15 21:52:53

前言

在Java并发编程学习中,定时器是必不可少的环节。

我们知道线程的调度是随机的,但是有的时候我们就是需要它有序一些,此时的定时器就可以很好的解决这个问题。它可以按照一定的先后顺序,将我们的任务依次执行。

目录

一.Java官方库中的定时器

二. 实现定时器

🎈第一步

🎈🎈第二步?

🎈🎈🎈第三步

三.进行测试


一.Java官方库中的定时器

在Java.Util 中,内置了定时器的实现

以下是代码展示:

package Timer;

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

public class Demo1 {
    public static void main(String[] args) {
        Timer t1 = new Timer();
        t1.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("我延迟3000ms执行");
                //线程并没有结束,是因为Timer内部的线程阻止了线程结束
            }
        },3000);
        t1.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("我延迟2000ms执行");
                //线程并没有结束,是因为Timer内部的线程阻止了线程结束
            }
        },2000);
        System.out.println("程序启动");
        t1.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("我延迟1000ms执行");
                //线程并没有结束,是因为Timer内部的线程阻止了线程结束
            }
        },1000);
    }


}

运行结果:

代码解读:1. 首先使用了Timer类,并且调用了Timer类中的schedule方法

? ? ? ? ? ? ? ? ? 2. schedule方法需要传入两个参数 :需要运行的任务和需要延迟的时间

? ? ? ? ? ? ? ? ? 3.由程序的运行结果可以看出,就算延迟 执行3000ms的语句在最前面,也会被? ? ? ? ? ? ? ? ? ? ? ? ? 放到最后来打印。

? ? ? ? ? ? ? ? ? 4.Timer类中其实内置了一个线程扫描器,可以判断出那些线程需要执行。

俗话说的好,自己动手丰衣足食。我们看到了这样的使用方法,可以去深刻理解它内部的实现。那么我们下面自己实现一个简单的定时器代码!

二. 实现定时器

?实现定时器的过程其实还是比较复杂的。它牵扯到JavaSE的语法,牵扯到泛型和数据结构中的知识。此处我会在写代码的时候,简单的为大家介绍一下。

首先要明确,我们要实现一个定时器,需要有一下三个必需品:

1. 需要有一个任务类,里面要有 任务 和 延迟的时间。当这个类被实例化的时候,初始化任务和时间。

2.需要有一个数据结构,这个数据结构可以按照我们自己指定的顺序来存放任务。那么很明显,最佳的人选就是 优先级队列。

3.需要有一个扫描线程,这个线程需要不停的扫描队头元素(因为队头元素是时间最近的,最需要执行的),看看是否到达执行的时间。

🎈第一步

首先实现一个任务类。

那么我们可以写出以下代码:

package Timer;

import java.util.Date;
import java.util.PriorityQueue;

class MyTimerTask implements Comparable<MyTimerTask> {
    //通过这个类,描述了一个任务
    //在这里写一个执行的任务
    private Runnable runnable;
    //写一个运行的时间
    private long time;

    //写一个构造方法,当使用MyTimerTask的时候,可以初始化任务和时间
    //此处的delay就是 schedule 传入的 相对时间
    MyTimerTask(Runnable runnable,long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }


    @Override
    public int compareTo(MyTimerTask o) {
        /**
         * 实现Comparable接口是为了对任务进行排序
         */
        //让队首元素是最小时间的值
        return (int) (this.time - o.time);
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }
}

代码解读:

1.我们首先写了一个MyTimerTask任务类,这个类实现了Comparable<>接口,既然实现了接口,?那就要重写里面的?compareTo 方法。这样的作用是为了使得每一个被实例化的任务类,都是可以被比较的。因为我们的任务是有先后执行顺序的!我们重写的compareTo方法,是让队首元素一定是最小时间的任务(迫切需要执行)。

2.这个类里面有?private Runnable runnable;? 这个意思是创建了一个Runnable 接口,但是还没有重写里面的run方法。它在等待一个重写了run方法的类~;最好的办法就是在初始化任务的时候,传入这个重写了run方法的类。

3.明白了第二点,那么也就知道MyTimerTask的构造方法为什么要这样写了。但是有一点需要注意的是,初始化的time应该是绝对时间。

绝对时间举例:假设现在是14.00 有个任务我们需要延迟30分钟执行,那么也就是14.30执行。此处的time就是 14.00?+ 30 ---> 14.30,System.currentTimeMillis()就是获取当前的系统时间。

🎈🎈第二步?

需要有一个优先级队列来按照我们指定的顺序来进行存放任务,这个队列我们可以放到实现扫描线程的类中。当初始化这个类的时候,就相当于初始化好了扫描线程和优先级队列。

那么我们可以写出以下代码:

public class MyTimer {

    //首先要有一个数据结构,这个数据结构可以将任务排序,将时间最短的排在队头
     protected PriorityQueue<MyTimerTask>  queue = new PriorityQueue<>();

    //使用锁对象,解决线程安全问题
    private Object locker = new Object();

    public void schedule(Runnable runnable,long delay) {
        //此处的schedule方法
        synchronized (locker) {
            queue.offer(new MyTimerTask(runnable, delay));
            locker.notify(); //此处放完元素 用来唤醒正在等待的线程
        }
    }

    //此处要写构造方法,当外部调用MyTimer的时候,直接初始化扫描锁
    MyTimer() {
        //创建一个扫描线程
        // 此处扫描线程的任务:扫描线程, 需要不停的扫描队首元素, 看是否是到达时间.
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                         //当队列为空的时候,需要等待另外的线程唤醒.
                        // 唤醒此线程的应该是往队列放任务的操作                           
                            locker.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();
    }

}

代码解读:

1.首先我们在这个类中内置了一个优先级队列,这个优先级队列是用来按照我们的规则存放任务的。

2.如何schedule方法有两个参数,一个是需要执行的任务(必须是实现了Runnable接口的子类),一个是需要延迟的时间。

3.在schedule方法中,主要的操作就是往优先级队列中放入元素。我们offer放入元素的时候?new MyTimerTask(runnable, delazy); 意思是在放入元素的时候 初始化MyTimerTask实例

4.我们写的构造方法是最复杂的,构造方法中内置了一个线程 t ,这个线程负责一直扫描队首元素,查看是否到达时间。其实扫描这个操作,就是一直判断队首元素是否到达了时间

那么我们在队列不为空的时候,需要一直判断(使用while循环)。如果到达了时间,那就执行,并且从任务队列中删除。如果没有到达执行时间,先一直判断。

5.使用加锁机制的原因也很简单:?

? ? ? ? ?此处我使用了锁对象的方式,其实锁对象也很简单。就是创建一个对象,这个对象是专门用来判断一段代码中的操作是否被加锁了。

?需要注意的是,其实只要实例化好MyTimer这个类,那么wait操作就已经开始执行了。因为此时的队列为空。一旦使用schedule方法,那么这时候有了一个任务,队列不为空。此时就可以解锁了。

🎈🎈🎈第三步

到现在,其实代码已经没有太大的问题了。但是我们仔细一想!如果队列中只有一个任务,假设现在的时间是14.00,我让他30分钟之后执行。那么其实它一直在判断是否到达了14.30,其实根本没这个必要。就好比我定了一个闹钟是14.30叫醒我,那么我一直在看是否到达了14.30,这种行为是很没必要的。这30分钟时可以让线程不做任何操作的,这样可以减少资源开销。 这种现象其实就是忙等现象。

那么我们可以使用wait来解决这个问题:

                        if (curTime >= task.getTime()) {
                            //说明当前任务已经到达了时间,开始执行
                            task.getRunnable().run();
                            //执行完毕从队列中剔除
                            queue.poll();
                        } else {
                            //此处是如果还没有到任务时间,暂时不执行任务
                            //此处使用wait的时间版本是为了避免一个线程忙等的情况
                            locker.wait(task.getTime() - curTime);
                        }

?此时如果还没到执行时间,那么就wait一定的时间。

值得注意的是,schedule方法中的notify有双重作用:

三.进行测试

至此为止,代码已全部写完,下面来测试

package Timer;

public class Test {
    //程序运行,只要这个timer被实例化,此时里面的扫描线程就已经被创建好了


    public static void main(String[] args) {
        //此时在主线程中试用
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000执行");
            }
        },3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000执行");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000执行");
            }
        },1000);
    }

}

运行结果:

是符合我们预想的结果的!

其实代码也挺复杂,执行过程也挺麻烦。可以看下我梳理的流程步骤:

文章来源:https://blog.csdn.net/qq_65501447/article/details/135023471
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。