【JavaEE初阶】第九节.多线程 (基础篇)定时器(案例三)

news2024/11/20 15:34:41

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

前言

一、定时器概述、

二、定时器的实现 

2.1 Java标准库 定时器的使用

 2.2 自己模拟实现一个定时器

 2.3 对自己实现的定时器的进一步优化

2.3.1 为何需要再进行优化

2.3.2 如何进行进一步优化(一) 

2.3.3 如何进行进一步优化(二)

2.4 附上自己模拟实现定时器的代码

总结



前言

提示:这里可以添加本文要记录的大概内容:

继多线程的阻塞队列之后,我们来学习一下 第三个经典的案例 —— 定时器;

定时器,也是开发中的一个比较常用的基础组件;

定时器,就像闹钟一样,闹钟在达到一个设定的时间以后,就会发出响声来提醒;而定时器到了设定好的时间以后,就会执行某个指定好的代码;

一、定时器概述

定时器,又称 任务定时执行器;

即:到了一定的时间,就会去执行指定的任务;

在 Java标准库中,提供了一个带有定时器功能的 Timer类,Timer类 的核心方法是 schedule方法

schedule方法 包含了两个参数,第一个参数为 即将要执行的任务代码,第二个参数为 指定多长时间后执行地一个参数中的代码(单位是 毫秒)

如:

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

二、定时器的实现 

2.1 Java标准库 定时器的使用

代码实现:

运行结果:

在观察运行结果之后,就会提出两个小问题:

问题一:程序执行完之后,为啥没有结束呢?

实现定时器,背后涉及到了多线程,Timer 里面有线程,这个线程的运行阻止了 进程的退出;


问题二:那为什么不直接使用 sleep 呢,还专门搞了一个定时器?

使用 sleep 是把当前线程给阻塞了;

换句话说,sleep 的时间里,啥也干不了,只能干等着;但是使用定时器,之前的线程该干啥干啥;

我们可以来证实一下,加一些代码并且观察运行结果:

运行结果:

 

我们从中可以看出,在 定时器定时的那段时间里,线程是在继续干活的,仍然在打印出 main

如果使用 sleep,那么这段时间就只能干等了

所以,平常使用的还是定时器较多

 

 2.2 自己模拟实现一个定时器

在模拟实现定时器之前,我们应该要思考清楚,一个定时器里面应该要有什么

首先,我们要清楚,一个 Timer 内部其实是可以加入很多很多任务的;

其次,Timer 里的每一个任务都要通过一定的方式描述出来(描述任务的过程 其实就相当于自己定义 TimerTask)

最后,还需要有一个线程,通过这个线程来扫描定时器 内部的任务,并执行其中时间到了的任务;

虽然当前的任务有很多,但是它们的执行顺序是一定的:按照时间顺序先后来执行;

此处 可以使用 优先级队列来执行,使用优先级队列可以非常高效的 找到当前时间最小的任务(首先要执行的任务)


代码实现:

package thread;
 
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;
 
//通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
    //获取具体任务
    private Runnable command;
    //执行任务时的时间戳
    private long time;
    //构造方法
    public MyTask (Runnable command,long after) {
        this.command = command;
        //我们希望知道完整的时间戳(几点几分几秒之后执行),所以用 当前系统时间戳+多长时间后执行 来表示
        //此处记录的是绝对时间戳,不是 "多长时间之后执行"
        this.time = System.currentTimeMillis() + after;
    }
    //执行任务的方法,直接在内部调用 Runnable 的 run 即可
    public void run() {
        command.run();
    }
 
    //获取执行时间
    public long getTime() {
        return time;
    }
 
    @Override
    public int compareTo(MyTask o) {
        //希望 时间小的在前面,时间大的在后面
        return (int) (this.time - o.time);
    }
}
//自己创建的定时器类
class MyTimer {
    //使用 优先级队列的数据结构 来保存若干个任务
    //但是调用 schedule 的时候,可能是多个线程调用,并不一定是单线程调用(比如说可以定多个闹钟)
    //所以使用 的队列应该是 带有优先级的阻塞队列 PriorityBlockingQueue
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    //command 表示要执行的任务是啥,after 表示多长时间之后来执行这个任务
    public void schedule(Runnable command,long after) {
        MyTask myTask = new MyTask(command,after);
        queue.put(myTask);
    }
 
    public MyTimer() {
        //在这里启动一个线程,来完成执行任务的工作
        //尤其是每次出队列,来执行时间最短的任务
        Thread t = new Thread(() -> {
            while (true) {
                //循环过程中,不断的尝试从队列中获取到队首元素
                //判定队首元素当前的时间是否就绪,时间有没有到点,如果就绪就执行,如果不就绪就不执行
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (myTask.getTime() > curTime) {
                        //时间还没有到,塞回到队列中
                        queue.put(myTask);
                    }else{
                        //时间到了,直接执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

上面就是定时器的模拟实现代码,现在就需要我们来小小测试一下效果:

//测试代码
public class Demo24 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
 
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2222");
            }
        },4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3333");
            }
        },6000);
    }
}

运行结果: 

 2.3 对自己实现的定时器的进一步优化

2.3.1 为何需要再进行优化

这个线程的执行,还是有一定的缺陷的:

如果队列为空的话,那么的确在调用 take 的时候,在阻塞 没有什么太大的问题

如果队列不为空的话,那么 它会执行下面的逻辑:

取出任务
比较时间
如果时间未到,就会插入任务回队列
但是,当插入任务回队列的时候 且时间未到,就会再次调用 myTask,进入循环

但是,由于队列不为空 且时间未到,那么 又是调用,......

这样的后果,就是 如果队列不为空的话,且如果没有到时间,那么 就会产生大量的循环

这就相当于,CPU 在不断的进行 "空转",但是 又没有实质性的任务再执行,相当于在 "盲等"

2.3.2 如何进行进一步优化(一) 

我们希望,当我们把任务塞回队列之后,不应该让它立刻循环下一次,应该让线程等一会儿再进入循环;


如果引入了 sleep方法,看起来是会让线程不盲等,但是会引入新的问题;

举个例子,比如说,定了一个闹钟,想要下午两点和同学一起出去看电影;看了一下时间,现在才是 10:50,于是 sleep 了一个小时,到了 11:50;再看了一下时间,于是又 sleep 了一个小时 ......

这时候,你突然想起还有一个很紧急的事情没有做 —— 做核酸,在 12:00 之前帮助做核酸的志愿者就要走了;

但是,在 11:50 ~ 12:50 这段时间中,你仍然在 sleep,就无法在执行这个任务;

直到 sleep 结束,这个 "做核酸" 的任务才会被执行(于是,你又要等到下个时间段在去做了);


所以说,我们需要想想其它的办法

虽然 sleep 不行,但是 wait 可以,相比于 sleep 来说,wait 是可以被提前唤醒的!!!

如果是能够提前唤醒,就完全可以再插入新任务的时候,把这里的等待给唤醒,再去执行新的任务;

(

于是又 sleep 了一个小时 ......

这时候,你突然想起还有一个很紧急的事情没有做 —— 做核酸,在 12:00 之前帮助做核酸的志愿者就要走了;

但是,在 11:50 ~ 12:50 这段时间中,你仍然在 sleep,就无法在执行这个任务;)就可以避免这种情况的发生!!!)

代码实现:

  

2.3.3 如何进行进一步优化(二)

经过上述的优化,已经差不多了,只剩下最后一个小小的问题了


有这样一种情况,假设当前的时间是 10:00,此时 取出的队首元素是 11:00 要执行的任务,此时 时间还没有到,于是 就把这个任务塞回到队列中;

接着 就需要等待 1h,但是,我们知道,多线程执行两个线程的顺序是不确定的,于是 在 put 之后,wait 之前,可能会有另外的新任务到达了,新的任务是在 10:30 运行;

当我们把 新的任务 插入之后,进行了 notify;

但是,由于 另外一个线程还没有 wait,所以这个 notify 就相当于空打了一炮;

于是,回到了wait方法继续执行,就等待了的是 1h,而不是等待了的是 30min;


 之所以出现这个问题,主要是出现了线程调度的问题,因此需要把锁的范围扩大一点就可以了

 注意:取元素、判断时间、put、wait 应该视为原子操作,如果在其中间插入任务,都可能会出现风险的;


当然,如果直接放大锁的范围的话,由于后面的 take 会让线程造成死锁的问题(因为在初始情况下 队列可能是空着的),就会让线程阻塞 ;

所以还需要一个判断条件:

2.4 附上自己模拟实现定时器的代码

package thread;
 
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;
 
//通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
    //获取具体任务
    private Runnable command;
    //执行任务时的时间戳
    private long time;
    //构造方法
    public MyTask (Runnable command,long after) {
        this.command = command;
        //我们希望知道完整的时间戳(几点几分几秒之后执行),所以用 当前系统时间戳+多长时间后执行 来表示
        //此处记录的是绝对时间戳,不是 "多长时间之后执行"
        this.time = System.currentTimeMillis() + after;
    }
    //执行任务的方法,直接在内部调用 Runnable 的 run 即可
    public void run() {
        command.run();
    }
 
    //获取执行时间
    public long getTime() {
        return time;
    }
 
    @Override
    public int compareTo(MyTask o) {
        //希望 时间小的在前面,时间大的在后面
        return (int) (this.time - o.time);
    }
}
//自己创建的定时器类
class MyTimer {
    //这个是用来阻塞等待的锁对象
    private Object locker = new Object();
    //使用 优先级队列的数据结构 来保存若干个任务
    //但是调用 schedule 的时候,可能是多个线程调用,并不一定是单线程调用(比如说可以定多个闹钟)
    //所以使用 的队列应该是 带有优先级的阻塞队列 PriorityBlockingQueue
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    //command 表示要执行的任务是啥,after 表示多长时间之后来执行这个任务
    public void schedule(Runnable command,long after) {
        MyTask myTask = new MyTask(command,after);
        //唤醒
        synchronized (locker) {
            queue.put(myTask);
            locker.notify();
        }
    }
 
    public MyTimer() {
        //在这里启动一个线程,来完成执行任务的工作
        //尤其是每次出队列,来执行时间最短的任务
        Thread t = new Thread(() -> {
            while (true) {
                //循环过程中,不断的尝试从队列中获取到队首元素
                //判定队首元素当前的时间是否就绪,时间有没有到点,如果就绪就执行,如果不就绪就不执行
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            //时间还没有到,塞回到队列中
                            queue.put(myTask);
                            //比如说,下午两点有新任务,现在 是十一点,那么 一减就可以得出 需要等待 三个小时就可以了
                            locker.wait(myTask.getTime() - curTime);
                        }else{
                            //时间到了,直接执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
public class Demo24 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
 
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2222");
            }
        },4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3333");
            }
        },6000);
    }
}
 
 
 
 

好了,关于多线程的第三个案例 —— 定时器就介绍到这里了; 

总结

今天就介绍到这里了,我们下一节再见!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/191253.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CMOS图像传感器——了解光圈

在之前有提到传感器英寸,也提到了曝光三要素之一的ISO,这里主要说明另外一个曝光三要素——光圈。在本文中,我们将介绍光圈及其工作原理。 一、什么是光圈 光圈可以定义为镜头中的开口,光线通过该开口进入相机。类比眼睛是的工作原理,就容易理解了:当人在明亮和黑暗的环…

【链表之单链表】

前言&#xff1a;链表是什么&#xff1f; 链表的操作 1.单链表的结构 2.头文件的包含 3.动态申请一个节点 4.单链表打印 5.单链表尾插 6.单链表头插 7.单链表尾删 8.单链表头删 9.单链表查找 10.单链表在pos位置之后插入x 11.单链表在pos位置之前插入x 12. 单链表…

【数据挖掘】基于粒子群算法优化支持向量机PSO-SVM对葡萄酒数据集进行分类

1.粒子群算法的概念 PSO是粒子群优化算法&#xff08;Particle Swarm Optimization&#xff09;的英文缩写&#xff0c;是一种基于种群的随机优化技术&#xff0c;由Eberhart和Kennedy于1995年提出。粒子群算法是模仿昆虫、兽群、鸟群和鱼群等的群集行为&#xff0c;这些群体按…

中国电子学会2021年03月份青少年软件编程Scratch图形化等级考试试卷三级真题(含答案)

2021-03Scratch三级真题 分数&#xff1a;100题数&#xff1a;38 一、单选题(共25题&#xff0c;每题2分&#xff0c;共50分) 1.在《采矿》游戏中&#xff0c;当角色捡到黄金时财富值加1分&#xff0c;捡到钻石时财富值加2分&#xff0c;下面哪个程序实现这个功能&#xff1…

【软件测试】资深测试总结的测试必备8点,堪称测试人的好莱坞大片......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试8板斧。测试8板…

Task8:Excel的数据可视化

目录一 条形图二 条件单元格格式三 迷你图四 练习题一 条形图 【例子】直观的展示销售额之间的差别 方法&#xff1a;【开始】–>【条件格式】–>【数据条】 【只想显示条形图&#xff0c;不想显示金额】 1.条形图区域—>条件格式—>管理规则 2.选择设置的规则&a…

单应性Homography梳理,概念解释,传统方法,深度学习方法

Homography 这篇博客比较清晰准确的介绍了关于刚性变换&#xff0c;仿射变换&#xff0c;透视投影变换的理解 单应性变换 的 条件和表示 用 [无镜头畸变] 的相机从不同位置拍摄 [同一平面物体] 的图像之间存在单应性&#xff0c;可以用 [透视变换] 表示 。 opencv单应性变换求…

Active Directory计算机备份和恢复

在Active Directory&#xff08;AD&#xff09;环境中&#xff0c;用户通过域中的计算机认证他们自身。从AD中删除这些计算机账户时&#xff0c;系统也会自动从域中删除它们。于是&#xff0c;用户不能再通过些计算机登录网络。为允许用户访问域资源&#xff0c;必须恢复这些已…

聚集千百个企业管理系统的API资产,打造API资产全生命周期一站式集成体验

API——接口&#xff0c;作为软件世界中的连接服务和传输数据的重要管道&#xff0c;已经成为数字时代的新型基础设施&#xff0c;是各领域驱动数字变革的重要力量之一。传统企业集成主要采用点对点或ESB集成方式&#xff0c;基于全新API战略中台的API新型集成方式通过解耦系统…

SpringBoot跨域请求解决方案详细分析

跨域的定义 跨域是指不同域名之间的相互访问&#xff0c;这是由浏览器的同源策略决定的&#xff0c;是浏览器对JavaScript施加的安全措施&#xff0c;防止恶意文件破坏。同源策略&#xff1a;同源策略是一种约定&#xff0c;它是浏览器最核心的也是最基本的安全策略&#xff0…

【数据产品】缓存设计

背景&#xff1a;为什么需要做缓存&#xff1f; 我所做的产品的指标设计越来越复杂&#xff0c;查询性能也随之下降。因此需要增加缓存层&#xff0c; 以提高接口查询效率。 哪些层需要做缓存&#xff1f; 随着指标系统的应用&#xff0c;该产品的查询逻辑也越来越简单&…

二分查找核心思路--单调性--极值

在最初的二分查找中&#xff0c;我们将一组数据按大小排序&#xff0c;然后根据arr[mid]与要查找的k的大小比较&#xff0c;从而每次去掉一半的数字&#xff0c;使时间复杂度简化为O&#xff08;logN&#xff09;。 排序本质上是让数据的单调性统一&#xff0c;变为单增或单减…

spring中的JSR-303统一校验

1.在前后端的传输参数的过程中数据在何处校验? 在前后端都需要进行校验,只是分工不同. 2.各个层的校验内容: 1.Controller层主要负责校验残水的合法性,包括: 必填的参数字段,数据格式的校验 2.Service层的业务校验是审核业务中的规则的相关内容,比如:课程已经审核通过所以提…

vue3 为何比 vue2 快

vue3 为何比 vue2 快 测试环境&#xff1a;https://vue-next-template-explorer.netlify.app/ 1、proxy 响应式 vue3 优缺点&#xff1a; 深度监听性能更好可监听 新增 / 删除 属性可监听数组变化Proxy 能规避 Object.defineProxy 的问题Proxy 无法兼容所有浏览器&#xff…

OAuth2介绍

目录 一、什么是OAuth2 二、OAuth2中的角色 三、认证流程 四、令牌的特点 五、OAuth2授权方式 授权码 隐藏方式 密码方式 凭证方式 一、什么是OAuth2.0 概念&#xff1a;第三方授权解决方案 OAuth2.0是目前使用非常广泛的授权机制&#xff0c;用于授权第三方应用获取…

[NRF52] mesh DFU

mesh DFU升级过程&#xff1a;完整流程&#xff1a;以前nRF SDK DFU的实现是通过nRF51 Dongle配合主机nRF connect工具&#xff0c;且借助Secure DFU的后台式更新速率较快&#xff08;见另一篇笔记&#xff09;&#xff0c;现在的nRF mesh DFU分角色&#xff0c;全都由DK充当&a…

什么是单体应用?什么是微服务?

Monolith&#xff08;单体应用&#xff09;&#xff0c; 也称之为单体系统或者是 单体架构 。就是一种把系统中所有的功能、模块、组件等耦合在一个应用中应用最终打成一个(war,jar)包使用一个容器(Tomcat)进行部署&#xff0c;通常一个应用享用一个数据库。 也就是将所有的代码…

Java版数据结构与算法笔记

文章目录一、数据结构与算法概述及题目1、数据结构和算法的关系2、线性结构与非线性结构Ⅰ-线性结构Ⅱ-非线性结构3、经典面试题Ⅰ-字符串匹配问题&#xff1a;Ⅱ-汉诺塔游戏Ⅲ-八皇后问题:Ⅳ-马踏棋盘算法4、几个实际编程中遇到的问题Ⅰ-字符串替换问题Ⅱ-一个五子棋程序Ⅲ-约…

这家芯片企业,从创立之初就用 Authing 管理身份

在德州仪器和苹果的经验&#xff0c;让我深知统一身份管理要从 Day 1 做起。——Alpha Cen 联合创始人 & CEO 王璠 案例亮点&#xff1a; 打通 2000 主流应用、满足芯片初创企业统一身份需求 一周快速上线&#xff0c;产品开箱即用&#xff0c;后续无需费力运维 基于协同…

Pytorch安装及环境配置详细教程(CUDA版本)

文章目录前言一、查看GPU支持的CUDA版本二、安装CUDA三、确定torch、torchvision和python版本四、安装anaconda五、安装torch和torchvision前言 安装cuda版本的pytorch时踩了不少坑&#xff0c;网上安装pytorch的版本很多&#xff0c;一般的教程都是到pytorch的官网&#xff0…