JAVAEE—实现多线程版本的定时器

什么是定时器

定时器的概念

首先定时器是什么呢?

定时器是我们在开发中比较常用的一个组件,类似于一个闹钟,当某个你设置的时间到了之后就开始去执行特定的任务。

定时器的简单应用和介绍

代码示例

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

public class Main {
    public static void main(String[] args) {
        Timer t1=new Timer();
        t1.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2秒以后打印");
            }
        },2000);
        System.out.println("我爱老婆");
    }
}

首先我们来看一下这个代码,Timer是java自身所拥有的的一个定时器类,而schedule是这里面的一个方法,方法的形参如图
在这里插入图片描述
根据图中我们可以看到这里面有一个TimerTask类还有一个long类型的整数,那么第一个是什么类呢?我们再往深一些去介绍
在这里插入图片描述
我们可以看到这个类是继承了Runnable接口的那么按照我们之前的学习这里面肯定有一个run方法,因此我们可以得出这里面的第一个参数我们传递的其实就是要进行的方法是什么,第二个参数就是我们设置的时间是在多少时间以后,那么上面写的实列代码表示的意思就是,两秒之后执行任务我们看一下代码运行结果。
在这里插入图片描述
在这里插入图片描述
这是运行截图我们可以看到两个现象,

首先,在执行任务期间,并不是主线程的代码需要等待其执行完毕后再继续执行整个过程感觉更像是多线程的模式执行的
其次:在执行任务之后当一切都打印完成后我们的程序并没有按照我们想象的那样退出而是在那里暂停了。

这是为什么呢?那么我们就来解析一下定时器的内部代码

定时器的代码解析

首先上面的两个问题我们逐个回答首先第一个

定时器在执行任务的时候是创建了一个线程去执行吗?

是的没错,定时器在执行任务的时候其实是创建了一个线程执行的。这个线程在我们创建对象的时候就已经创建好了,这个线程的名字我们称之为扫描线程。

为什么叫做扫描线程呢?

这里我先请大家想一个问题就是你为了能早其会不会订多个闹钟把自己叫醒,我相信即使你自己可能不会但是你身边一定有这种人对不对。那么这时候我们就要明白一个问题了定时器这个东西并不是只能定一个时间,而是可以定多个时间多个任务,我们只需要在到达某个时间之后执行这个需要执行的任务就可以了。那么既然我们可以定多个任务多个闹钟的话我们该用什么容器去存储呢?
有的同学说是顺序表?此言差亦假如说我们用了顺序表那么每次我想知道那个任务到达该执行的时间了我岂不是还需要一个任务一个任务的去遍历吗?那假如说我们有很多个定时任务这可该怎么办呢?因此顺序表这个办法行得通但是效率太低了。
那么我们到底该用什么办法呢?难道就要你失败了吗?就要止步不前了吗?不肯定不是那么容易被打败的。
这时候我们想起来既然我们加入的是时间那么,时间在计算机眼中就是一个整数,既然如此那他肯定是可以比较大小的既然可以比较大小那就好办了我只需要用一个堆来进行存储就可以了我简直太聪明了,而事实也正是如此。我们存储任务用的数据结构就是堆。
而扫描线程就是不断的获取堆顶元素,然后查看堆顶的元素是否达到了执行的条件。从而开始执行任务。
正因为他不断的扫描因此我们叫做扫描线程。

执行完任务之后代码就暂停了不自动结束吗?

是的当我们的任务执行完了之后代码就暂停了不会自动结束需要我们手动结束这时候就有同学蒙蔽了,啊?这是为啥?那么我们接下来来动手实现以下这个定时器就知道原因了。

手撕定时器demo

首先我们先来看看库里面怎么实现的。
在这里插入图片描述
首先库里面的定时器的名字叫做Timer,然后schedule方法中的对象名字叫做Timetask因此我们也实现这两个类。那么首先我们先实现Timetask类,那么我们先来看看库里面这个类的构造方法。
在这里插入图片描述

在这里面我们设置了两个变量一个是记录时间的time还有一个就是我们的runnable。然后构造方法就是将传入的runnable赋值给类中的runnable然后主要是这个时间是怎么回事为什么要这样去写的呢?这就涉及到两个概念了。那就是相对时间与绝对时间的概念

相对时间与绝对时间

什么是相对时间呢?相对时间就是我们说的几秒几秒之后,这些几秒之后都是相对于我们现在的时间的说法因此叫做相对时间那么什么是绝对时间呢?绝对时间的意思就是我们说的,几点几分几秒这种叫做绝对时间,那么我们在使用的时候是用相对时间好呢?还是绝对时间好呢?我们可以带入我们生活中的列子,我们在生活中肯定是绝对时间要相对于相对时间是更加准确的,其次我们可以想一下自己定闹钟的时候是定一个几小时之后比较方便还是顶一个确定的几点几分比较方便呢?很显然是后者比较方便,那么如此我们就知道了这两个概念,那么我们就可以解释以下这个代码了。

System.currentTimeMillis()这个方法是用来获取我们当前的时间的我们来看一下这个方法的返回值如下图
在这里插入图片描述
我们看到这个方法的返回值是一个long类型的整数我们来打印以下。

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

public class Main {
    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(System.currentTimeMillis());
    }
}

运行结果
在这里插入图片描述
我们可以看出来这个方法可以获取当前的时间并将其转换为秒数。因此我们的初始化方法可以先获取当前的时间再加上我们想要定时的秒数然后获取到绝对时间从而进行初始化。因此这里的初始化是这样的格式。

Mytime类

接下来我们来实现Mytimer。那么在实现之前我们依旧先来分析一下怎么实现构造函数. 那么我们首先思考一下上面说的那句话那就是定时器内部是存在一个扫描线程的这个扫描线程从我们创建这个类的实列化对象的时候这个线程就已经创建好了说明这个线程肯定是在构造方法中被创建的, 既然如此, 我们就可以得出在构造方法中创建线程并且让这个线程不断的运行着. 由此我们的代码初步就可以写成下面的样子

import java.util.PriorityQueue;

public class Mytimer {
    private PriorityQueue<MyTimerTask>queue=new PriorityQueue<>();
    public  Mytimer(){
        Thread t1=new Thread(()->{
            while(true){
                
            }
        });
    }
    public void  schedule(Runnable runnable,long time){
        queue.offer(new MyTimerTask(runnable,time));
    }
}

首先构造函数创造线程, schedule方法将要执行的任务入队列, 那么接下来呢?很简单其实, 我们只需要让扫描线程不断的从这个堆里取出堆顶元素并判断是否可以执行,如果可以执行就执行如果不可以执行就不执行. 那么该怎么实现呢?首先就是我们要实现一个多线程版本的那么我们肯定要明白一个事情那就是当我们的扫描线程发现这个线程为空的时候要进行等待那么按照我们上一篇将的生产消费者模型我们进行等待可以使用while循环判断当其为空的时候就一直等待
在这里插入图片描述
那么既然需要等待那就肯定需要上锁那么代码就是
在这里插入图片描述
好的那么等待的话我们入队列这个操作也需要上锁因为我们实现的是多线程版本的,因此为了保证安全我们需要堆入队列这个操作也上锁.
在这里插入图片描述
然后当我们入队列之后发现此时队列肯定不是空了我们就可以唤醒线程让其获取堆头的任务看看是否需要完成.
在这里插入图片描述
当我们判断堆顶任务需要执行后我们就使其执行,执行之后将其从堆中去除即可.我们用代码来试一下现在写的程序

import java.util.Objects;
import java.util.PriorityQueue;

public class Mytimer {
    private Object locker=new Object();
    private PriorityQueue<MyTimerTask>queue=new PriorityQueue<>();
    public  Mytimer(){
        Thread t1=new Thread(()->{
            try {
                while(true){
                    synchronized (locker){
                        while(queue.isEmpty()){
                            locker.wait();
                        }
                        //当可执行到这里的时候说明线程不为空我们需要进行一个操作就是取堆顶
                        MyTimerTask myTimerTask=queue.peek();
                        long nowTime=System.currentTimeMillis();
                        if(nowTime>=myTimerTask.getTime()){
                            Runnable runnable=myTimerTask.getRunnable();
                            runnable.run();
                            queue.poll();
                        }else{

                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
    }
    public void  schedule(Runnable runnable,long time){
        synchronized (locker){
            queue.offer(new MyTimerTask(runnable,time));
            locker.notify();
        }
    }
}

Test

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

public class Main {
    public static void main(String[] args) {
       Mytimer mytimer=new Mytimer();
       mytimer.schedule(new Runnable() {
           @Override
           public void run() {
               System.out.println("2000");
           }
       },2000);
       mytimer.schedule(new Runnable() {
           @Override
           public void run() {
               System.out.println("1000");
           }
       },1000);
        mytimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);

    }
}

运行截图
在这里插入图片描述
那么现在回到上面的那个问题

为什么定时器必须我们手动结束代码的运行

相信写道这里大家就明白为什么了因为我们线程里的任务写的是while(true)并且当堆为空的时候我们的线程是在不停的等待的所以肯定不能自己结束需要我们手动才可以结束的啊.

改进版本

那么写道这里我们发现我们的目的能完成了我们的任务也正常显示了可是这里面有一个不完美的地方如图
在这里插入图片描述
那就是这个代码,我们举个例子假如说我们接下来要执行的任务是14:30分执行的那么现在没到这个时间的时候我们这个while循环岂不是一种都在运行着并且一直都是else然后什么也不干,这对我门的cpu资源是一个很大的浪费因此我们可以改一下怎么改呢?那就是wait的另一种用法.wait(最大等待时间) 那么代码如下图
在这里插入图片描述
在这里我们执行wait就可以了,那么有同学就会疑惑为什么不用sleep呢?因为如果用sleep的话我们就必须等到sleep结束可实际上呢假如说此时我们的堆顶元素是14:30执行的此时是14:09分不执行然后让他一直沉睡到14:30那么在其sleep的过程中加入我插入一个14:10分执行的任务的话无法将其唤醒就会导致这个14:10分的任务无法执行这是不被允许的.

为什么不用阻塞队列来实现呢?

因为我们知道这里的等待有两处而我们也希望可以用一个notify去唤醒这两处等待,但是阻塞队列内部已经有wait和notify了如果用notify 的话就会导致我们实现这样的两处等待一个notify唤醒就比较麻烦.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值