【JavaEE初阶】多线程案列之定时器的使用和内部原码模拟

前言:

🌈上期博客: 【JavaEE初阶】深入理解多线程阻塞队列的原理,如何实现生产者-消费者模型,以及服务器崩掉原因!!!-CSDN博客

🔥感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

⭐️小编会在后端开发的学习中不断更新~~~

🥳非常感谢你的支持

目录

[📚️ 1.内容简介](#📚️ 1.内容简介)

📚️2.定时器的使用

2.1使用场景

2.2标准库的方法

2.3实现运行

📚️3.定时器的模拟(重点)

3.1使用的数据结构

3.2任务类

1.属性

2.实现runnable类中的run方法

3.构造方法,在时间扫描中传递参数

4.实现时间比较器

3.3时间扫描类

1.属性

2.实现任务存储

3.构造方法,实现线程自动启动

3.4主函数

3.5线程安全问题

3.6实现线程阻塞和唤醒

1.优先级队列为空

2.时间未到

3.7run方法如何执行任务

📚️4.总结

📚️ 1.内容简介

🌈Hello!!!uu们,本期小编主要是讲解Java标准库中的一个重要的东西即定时器;

1.定时器在Java标准库中使用方法调用;

2.如何自己在idea上直接手搓实现一个定时器的功能模拟;

📚️2.定时器的使用

2.1使用场景

定时器就是日常生活中常用的组件~~类似于闹钟一样,即设定一个时间,当时间一到那么就会自动执行所规定的任务;

例如:咱们博客上的定时发布文章一样;

即在我们发布博客的时候,存在一个定时发布的选项,这就是定时器在我们之间存在的地方,当然还有我们日常生活中的智能家居,定时完成某个任务~~~,小编就不再过多举例了;

2.2标准库的方法

在Java标准库中,提供了定时器的方法,这里我们就要实施化定时器的对象;

代码如下:

java 复制代码
Timer timer=new Timer();

然后调用schedule方法,重写run方法,规定我们要执行的任务,以及执行任务的时间;

代码如下:

java 复制代码
 timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("此时时间为3秒后");
            }
        },3000);

注意:这里的3000单位是ms级别的,表示3秒,即3秒后再次执行这个任务;

2.3实现运行

这里小编设置了两个任务,以及两个任务执行的时间,看看最后的运行结果;

代码如下:

java 复制代码
public class testDemo23 {
    public static void main(String[] args) throws InterruptedException {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("此时时间为3秒后");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("此时时间为2秒后");
            }
        },2000);

此时运行后的演示结果如下图所示:

此时我们可以看到任务的执行依据后面的时间来进行的;

注意:此时进行没有结束,说明这个Timer是一个前台线程(小编上期有讲),这里就是timer不知道是否还有其他的任务,时刻准备着~~~

当然我们是可以一手动来将这个线程结束掉;

代码如下:

java 复制代码
Thread.sleep(3000);
timer.cancel();

注意:这里的休眠是为了保证任务能够执行完,cancel是为了将这个线程转化为后台线程,main函数执行完后,这个线程也跟着结束;

📚️3.定时器的模拟(重点)

3.1使用的数据结构

我们要明白这个定时器的模拟过程:

1.实现一个线程,负责进行扫描任务,并且还要进行"掐"时间这个过程,当时间一到就运行

2.实现一个数据结构,存储schedule进来的方法

这里线程就要进行这个数据结构的扫描,若时间到了就执行,但是这里的时间复杂度为log(N) 所以这里我们就要使用优先级队列了;

原因:我们可以设置一个比较方法来比较时间,由于先执行的一定是时间最少的,那么时间少的就为队首元素,这里我们就可以直接取队顶元素,这时候我们的时间复杂度为log(1);

3.2任务类

1.属性

代码如下:

java 复制代码
class MyTimerTask  {
    // 在什么时间点来执行这个任务.
    // 此处约定这个 time 是一个 ms 级别的时间戳.
    private long time;
    // 实际任务要执行的代码.
    private Runnable runnable;

注意:这里我们是持有一个runnable类,也可以实现继承,这里的time是一个毫秒级别的时间戳

2.实现runnable类中的run方法

如下代码:

java 复制代码
public void run() {
        runnable.run();
    }

注意:这里使用run方法,就是为了在主函数上执行对应的任务;

3.构造方法,在时间扫描中传递参数

代码如下:

java 复制代码
public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        // 计算一下真正要执行任务的绝对时间. (使用绝对时间, 方便判定任务是否到达时间的)
        this.time = System.currentTimeMillis() + delay;
    }

注意:这里的delay是一个相对的时间,而在后面比较是绝对的时间

4.实现时间比较器

代码如下:

java 复制代码
public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);       
    }

注意:由于利用的是优先级队列,所以这里要规定一个比较的条件,注意还要实现comparable接口

3.3时间扫描类

1.属性

代码入下:

java 复制代码
class MyTimer {
    // 负责扫描任务队列, 执行任务的线程.
    private Thread t = null;
    // 任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 搞个锁对象, 此处使用 this 也可以.
    private Object locker = new Object();

注意:这里的线程是为了保证以下线程启动,即当使用这个类时,线程就要启动,这里还有一个优先级队列的声明,和防止线程安全问题设置的一个锁对象

2.实现任务存储

代码如下:

java 复制代码
public void schedule(Runnable runnable, long delay) {
        MyTimerTask task = new MyTimerTask(runnable, delay);
        queue.offer(task);           
        
    }

注意:这里就是将执行的任务传给mytimertask然后保存到优先级队列中

3.构造方法,实现线程自动启动

代码如下:

java 复制代码
// 构造方法. 创建扫描线程, 让扫描线程来完成判定和执行.
    public MyTimer() {
        t = new Thread(() -> {            
            while (true) {                                   
                        while (queue.isEmpty()) {
                            // 暂时先不处理                           
                        }
                        MyTimerTask task = queue.peek();
                        // 获取到当前时间
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 当前时间已经达到了任务时间, 就可以执行任务了.
                            queue.poll();
                            task.run();
                        } else {
                            // 当前时间还没到, 暂时先不执行                                                                                
                        }                    
                }             
        });       
       t.start();
    }

注意:这里线程要不断进行扫描队列,如果时间到了就执行,没有到就先不做处理,任务执行了就删除,这里要进行时间的比较,若此时时间大于或等于了规定时间,那么就执行

3.4主函数

实现规定任务,时间,代码如下:

java 复制代码
public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        }, 3000);
    }

这里的任务就是run方法里的打印,以及3000所规定的时间要求;

3.5线程安全问题

这里由于是两个线程,一个扫描,一个添加任务线程,那么此时我们可以发现,在线程执行的过程中存在读和写的操作,此时就要进行加锁;

java 复制代码
while (true) {             
        synchronized (locker) {
                while (queue.isEmpty()) {
                     // 暂时先不处理
                     
                }

注意:这里就要进行加锁打包,小编后面就省略了,当然这里任务添加方法也是需要进行加锁操作的;

3.6实现线程阻塞和唤醒

1.优先级队列为空

**问题:**我们可以发现当队列为空的时候,现场因该进入阻塞的状态,否则直接执行以下代码释放锁之后,会直接导致释放锁又拿到锁,导致线程饿死

所以这里在队列为空的时候我们需要进行线程阻塞,然后再适合的时间进行唤醒:

代码如下:

java 复制代码
while (queue.isEmpty()) {
           // 暂时先不处理
           locker.wait();
}

此时唤醒就应该在添加任务之后,所以我们要在添加任务时进行线程的唤醒

代码如下:

java 复制代码
 public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            // 添加新的元素之后, 就可以唤醒扫描线程的 wait 了.
            locker.notify();
        }
    }

那么此时添加任务后,线程唤醒,就可以进行执行了;

2.时间未到

**问题:**当时间没有到的时候我们应该进行等待,并且这里的等待是有时间要求的,即超时等待;

代码如下:

java 复制代码
if (curTime >= task.getTime()) {
       // 当前时间已经达到了任务时间, 就可以执行任务了.
       queue.poll();
       task.run();
 } else {                            
       locker.wait(task.getTime() - curTime);
 }

注意:进行等待的时间就是这个任务所执行的时间要求,若在此过程中添加了其他更早执行的任务,那么这里的任务添加类就能够进行唤醒的操作;

这里不能使用sleep,因为当来新的任务后,线程不能唤醒解锁,导致错过新的任务,如果是continue的话就会循环执行任务那么此时就叫"忙等"

3.7run方法如何执行任务

小编先将代码执行顺序归为一整个,给小伙伴们讲解一下:

java 复制代码
timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是一个3秒后的任务");
            }
        }, 3000);


public void schedule(Runnable runnable, long delay) {     
            MytimerTask task = new MytimerTask(runnable, delay);          
        }


public MytimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;     
    }


public void run() {
        runnable.run();
    }


 task.run();

1.首先我们在主函数规定runnable对象重写run方法的执行任务后,传给schedule

2.在mytimer类中的schedule方法接收参数后给mytimertask类的参数

3.在mytimertask类中接收到传递的参数后,在调用run方法,此时的run方法就是在主函数实现的重写任务方法

4.所以我们只需要在线程执行中通过mytimertask的对象,调用这个方法就好了;

如下图所示:

📚️4.总结

💬💬小编本期主要讲解了关于定时器在Java标准库中的使用方法,以及自主实现了关于定时器的代码模拟,当然这部分是有一定的难度的,这里涉及到"优先级队列,函数的调用,runnable类的使用,以及比较器的设定,线程安全问题,和唤醒阻塞"相关的知识体系,需要各位uu学习了解;

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

😊😊 期待你的关注~~~

相关推荐
开心工作室_kaic9 分钟前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it10 分钟前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
懒洋洋大魔王10 分钟前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康15 分钟前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神1 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
宅小海1 小时前
scala String
大数据·开发语言·scala
qq_327342731 小时前
Java实现离线身份证号码OCR识别
java·开发语言
锅包肉的九珍1 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦1 小时前
Scala的Array(2)
开发语言·后端·scala
yqcoder2 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript