JavaEE——自主实现计时器

news2024/11/27 12:43:32

文章目录

  • 一、认识定时器
  • 二、自主实现定时器
    • 1.明确定时器的内核原理
    • 2.定时器框架搭建
    • 3.优先级队列中的比较问题
    • 4.“忙等”问题
    • 5. 代码中随机调度的问题
  • 三、整体代码罗列

一、认识定时器

  1. 什么是定时器
    定时器是我们在日常的软件开发中很重要的一个组件。类似于闹钟,当到达设定时间后就去执行某个指定的代码。
    举一个非常常见的例子,在网络编程中,经常会出现 “卡了”、“连不上” 这样的情况,此时定时器就会设定在多长时间之后进行重连操作,或者停止等待,这就是定时器的一个简单使用。

2.标准库中的定时器运用

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

public class ThreadDemo {
    public static void main(String[] args) {
        System.out.println("程序启动");
        //Timer 是标准库的定时器
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }
}

在这里插入图片描述

二、自主实现定时器

1.明确定时器的内核原理

根据上面标准库中对定时器的使用,我们可以知道,要实现一个定时器需要实现以下功能:

  1. 让被注册的任务在指定的时间被执行。
  2. 定时器可以注册 N 个任务,这 N 个任务会按照最初的设定的时间,按顺序执行。

针对第一点:要在指定时间内执行线程。
要满足这个条件,需要单独在定时器内部设定一个扫描线程,让这个扫描线程周期性对任务进行扫描,判断是否到达时间,是否执行。

针对第二点:定时器可以注册 N 个任务。
很显然,这里需要一个数据结构来保存。
我们知道,这里的每个任务都带着一个重要元素 时间,并且要求 时间越靠前,越先执行。 到此,已经非常明显,优先级队列无疑是很好的一个选择。

在已知使用 优先级队列 之后,线程扫描也变得更加容易实现,这种情况下,只需要扫描队首元素即可,无序遍历整个队列。

2.定时器框架搭建

如图所示:
在这里插入图片描述
要实现一个阻塞式优先级队列,需要指定元素类型,这里的 任务 可以使用 Runnable 来表示,除此之外,我们还需要描述任务什么时候执行。

根据上面的描述,我们已经有了大致的方向,下面,我们先实现一个类,将 任务时间 进行包装,成为阻塞式优先级队列的元素类型,代码如下:

实现类型方法

class MyTask{
    //要执行的任务内容
    private Runnable runnable;
    //任务在什么时间执行(使用毫秒级时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取任务当前的时间
    public long getTime(){
        return time;
    }

    //执行任务
    public void run(){
        runnable.run();
    }
}

对于定时器类,需要提供一个 schedule 方法来实现任务的注册,代码如下:
实现创建任务方法

    //指定两个参数
    //第一个是指定任务
    //第二个是指定在多少秒后执行
    public void schedule(Runnable runnable,long after){
        //这里要注意的是,after 是要在当前时间下,在等待多长时间,这里的时间戳获取当前的时间
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(myTask);
    }

上面的这个方法还是比较简单的,对于实现定时器真正比较麻烦的方法,是在扫描线程上。代码如下:

实现扫描线程

    //扫描线程
    private Thread t = null;
    //扫描线程实现
    public MyTimer(){
        t = new Thread(()->{
            while(true){
                //取出队首元素,查看队首元素是否到达时间
                //如果时间没到,将元素放回到任务队列中
                //如果时间到,将任务执行
                try {
                    //取出队首的元素
                    MyTask myTask = queue.take();
                    //获取当前的时间
                    long curTime = System.currentTimeMillis();
                    //当前的时间与之前设定的时间比较
                    if(curTime < myTask.getTime()){
                        //还没到点,不必执行
                        //假设现在10:00 取出的任务要 11:00 执行
                        queue.put(myTask);
                    }else{
                        //表明时间到了,可以执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

3.优先级队列中的比较问题

到此,代码的大致逻辑已经实现完毕,让我们创建几个线程简单运行一下,如图:
在这里插入图片描述
不出所料,出现问题了,正如上面红线表示出来的一样,在这里,我们虽然使用了优先级队列,但是,我们没有设定这个优先级队列如何进行比较
所以,在这里我们需要 MyTask 类实现 Comparable 接口或者使用 Comparator 单独实现一个比较器。

这里我使用 comparable 接口实现比较,代码如下:

class MyTask implements Comparable<MyTask>{
    //要执行的任务内容
    private Runnable runnable;
    //任务在什么时间执行(使用毫秒级时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取任务当前的时间
    public long getTime(){
        return time;
    }

    //执行任务
    public void run(){
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        //返回 小于 0,大于 0,等于 0
        //this 比 o 小 返回 <0
        //this 比 o 大 返回 >0
        //this 等于 o 返回 0
        //当前需要的是队首元素是时间最小的元素
        return (int) (this.time - o.time);
    }
}

再次尝试运行,如图:
在这里插入图片描述

代码修改到这里,上面这个比较明显的问题已经解决,但是,代码中仍然潜藏着比较麻烦的问题。

4.“忙等”问题

我们知道,计时器会将队首的元素进行获取并且判断任务是否已将到达设定时间,但是,在我们这里的扫描线程这段代码中,会出现当任务的时间没到,会一直重复做拿出塞回的操作。
上述现象称之为 “忙等”。也就是: 等,但是没有闲着。
按理来说,等待是要释放 CPU 资源的。但是忙等,既进行了等待,又占用着 CPU 的资源。

针对当前的情况,忙等显然是不必要的,需要将其修改为阻塞式等待

呢么此处进行等待使用 sleep 方法可行吗? 时间要设定为多久呢?
假设当前时间为 10:00 队首元素要在 11:00 执行,呢么就直接设定等待 1 小时。这样其实过于大意了,因为在这 1 小时之中,随时都有可能会有新的任务加入,万一加入的任务等待的时间更短呢?
所以 sleep 方法在这里就显得很是死板了。

因此,wait 方法在这里就比较合适了。
首先,使用 wait 等待时,每次有新任务创建时,可以使用 notify 释放一下,重新检查时间,重新计算需要等待的时间。
其次,wait 方法提供一个带有 超时时间 的版本。如果没有新任务,这个版本的 wait 就可以等待到规定时间后进行自动唤醒。

所以,综上所述,这里使用 wait,notify 方法解决问题。
扫描线程中的改动
在这里插入图片描述
schedule 方法中的改动

在这里插入图片描述
写到这里,定时器中 90% 的问题已经解决,最后我们在考虑一个极端情况,这个情况和随机调度密切相关。

5. 代码中随机调度的问题

上面的问题中,我们解决了 “忙等” 这个问题,但是,因为线程的随机调度,代码中仍然存在着问题,如下图所示:
在这里插入图片描述

正如上面所讲,此时如果在线程被调走这段时间中添加了新的元素,就会出现线程安全问题。

正如上图所示,此时由于扫描线程中的 wait 操作还没有执行。当恰好在这个时间间隙中调用 schedule 方法,此时 schedule 方法中的 notify 方法将其不到任何唤醒 wait 的作用。 但是,任务仍然插入到了队列之中!!

注:
在这里插入图片描述
如上图所示,创建任务时,任务的时间设定在此处。
在这里插入图片描述
在 schedule 方法中通过构造方法获取该任务的时间。

场景设想:

  • 我们假设此时是 13:00.
  • 原本队首的元素要求 14:00 执行,即要等待 1 小时
  • 调度间隙中突然插入的任务要求 13:20 执行,即要等待 20分钟

如上面的解释和场景的设想,不难发现,此时此刻,新任务虽然已经插入队列,并且存在于队首,但是,此时代码中的时间计算仍然以未插入元素前的任务时间为基准进行等待。 新的任务,被错过了。

对上述场景的分析,我们知道,出现线程安全问题中有一点就是,原子性
在这里 take 操作和 wait 操作并不是原子性的,此时只要在 wait 和 take 之间进行加锁,这个问题就引刃而解了。如图所示:
在这里插入图片描述
总得来说,就是确保在每次 notify 的时候保证 wait 的确存在。

三、整体代码罗列

import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask>{
    //要执行的任务内容
    private Runnable runnable;
    //任务在什么时间执行(使用毫秒级时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取任务当前的时间
    public long getTime(){
        return time;
    }

    //执行任务
    public void run(){
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
    //这里主要通过实验观察,无需记背
        //返回 小于 0,大于 0,等于 0
        //this 比 o 小 返回 <0
        //this 比 o 大 返回 >0
        //this 等于 o 返回 0
        //当前需要的是队首元素是时间最小的元素
        return (int) (this.time - o.time);
    }
}

//自主实现定时器
class MyTimer{
    //扫描线程
    private Thread t = null;

    //使用一个阻塞式的优先级队列,保存任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //扫描线程实现
    public MyTimer(){
        t = new Thread(()->{
            while(true){
                //取出队首元素,查看队首元素是否到达时间
                //如果时间没到,将元素放回到任务队列中
                //如果时间到,将任务执行
                synchronized (this){
                    try {
                        //取出队首的元素
                        MyTask myTask = queue.take();
                        //获取当前的时间
                        long curTime = System.currentTimeMillis();
                        //当前的时间与之前设定的时间比较
                        if(curTime < myTask.getTime()){
                            //还没到点,不必执行
                            //假设现在10:00 取出的任务要 11:00 执行
                            queue.put(myTask);
                            //在 put 之后进行 wait 操作
                            synchronized (this){
                                //在获取后时间未到就阻塞等待,等待设定时间与当前时间之差的时长
                                this.wait(myTask.getTime() - curTime);
                            }
                        }else{
                            //表明时间到了,可以执行任务
                            myTask.run();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }

    //指定两个参数
    //第一个是指定任务
    //第二个是指定在多少秒后执行
    public void schedule(Runnable runnable,long after){
        //这里要注意的是,after 是要在当前时间下,在等待多长时间,这里的时间戳获取当前的时间
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(myTask);
        //在插入元素后就进行唤醒
        synchronized (this){
            this.notify();
        }
    }
}

public class ThreadDemo25 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }
}

结果展示:
在这里插入图片描述

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

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

相关文章

毫米波雷达数据采集

目录 1.数据采集方式2.分析数据格式3. 解读原始数据4.Bin文件格式 1.数据采集方式 数据采集有两种方式&#xff1a; 方式一&#xff1a;使用SDK中包含的Capture Demo&#xff1a; how to save raw data from the Capture Demo using Code Composer Studio(CCS) 在CCS中通过…

Python自动化测试框架有哪些?怎么选

目录 自动化测试框架概念 自动化测试框架根据思想理念和深度不同&#xff0c;渐进式的分为以下几种&#xff1a; 模块化测试脚本框架&#xff1a; 测试库框架&#xff1a; 数据驱动测试框架&#xff1a; 关键字驱动或表驱动的测试框架&#xff1a; 混合测试自动化框架&am…

软考A计划-软件设计师笔记

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

2023自动部署神器——Jenkins全网最全攻略

​ 大纲 ​ 1.背景 在实际开发中&#xff0c;我们经常要一边开发一边测试&#xff0c;当然这里说的测试并不是程序员对自己代码的单元测试&#xff0c;而是同组程序员将代码提交后&#xff0c;由测试人员测试&#xff1b; 或者前后端分离后&#xff0c;经常会修改接口&#xff…

从0到1腾讯云服务器使用教程(新手入门)

腾讯云服务器使用教程包括注册账号实名认证、选择云服务器CVM或轻量应用服务器CPU内存带宽和系统盘配置、安全设置和云服务器远程连接、安全组端口开通教程、云服务器环境部署以搭建网站为例手把手网站上线&#xff0c;云服务器文件传输和数据备份以及技术支持等详细说明&#…

Maven学习笔记(上)22版

1. 概述部分 1. 什么是 Maven&#xff1f; 为什么要学习Maven&#xff1f; 管理规模庞大的 jar 包&#xff0c;需要专门工具。脱离 IDE 环境执行构建操作&#xff0c;需要专门工具。 1、构建 Java 项目开发过程中&#xff0c;构建指的是使用『原材料生产产品』的过程。 原…

Linux高级---ingress

文章目录 一、ingress介绍二、ingress的工作原理三、ingress的使用1、搭建ingress环境2、准备service和pod3、创建http代理4、创建https代理 一、ingress介绍 在前面课程中已经提到&#xff0c;Service对集群之外暴露服务的主要方式有两种&#xff1a;NotePort和LoadBalancer&a…

【Linux】下的权限管理/关于root用户和普通用户的区别/不同用户的访问权限有什么不同?/到底什么是粘滞位?

本文思维导图&#xff1a; 文章目录 前言1.Linux下用户的分类sudo指令1.1文件访问者的分类&#xff08;人&#xff09; 2.文件类型和访问权限&#xff08;事物属性&#xff09;2.1Linux下的文件类型2.2 文件的权限属性&#xff08;角色/身份&#xff09;chmod指令和chown指令/…

燕千云ChatGPT应用,用过的都说香

本期受访人物&#xff1a;张礼军 甄知科技联合创始人&#xff0c;CTO 首席产品官 2022年底&#xff0c;基于人工智能技术驱动的自然语言工具横空出世&#xff0c;一经推出&#xff0c;ChatGPT迅速火遍全球&#xff0c;几乎各行各业都在探索ChatGPT具体业务场景的应用&#xf…

(万字长文)Linux——IO之重定向+缓冲区 +重定向 +缓冲区原理实现 +带重定向的简易版shell+标准输出标准错误

索引 文件描述符分配规则重定向 缓冲区1.什么是缓冲区2.缓冲区在哪里 重定向源码模拟实现缓冲区原理带重定向的简易版Xshell标准输入和标准错误 文件描述符分配规则 文件描述符的分配规则 从头遍历数组fd_array[],找到一个最小的&#xff0c;没有被使用的下标&#xff0c;分配…

yii Yii Framework PHP 框架

基于组件、开发大型 Web 应用的PHP 框架 Yii Framework是一个基于组件、用于开发大型 Web 应用的高性能 PHP 框架。Yii提供了今日Web 2.0应用开发所需要的几乎一切功能。Yii是最有效率的PHP框架之一。Yii是创始人薛强的心血结晶&#xff0c;于2008年1月1日开始开发。 中文名 …

Linux LAMP(Linux Apache MySQL PHP)搭建 Discuz! 网络论坛

LAMP架构 LAMP架构是目前成熟的企业网站应用模式之一&#xff0c;指的是协同工作的一整套系统和相关软件&#xff0c;能够提供动态Web站点服务及其应用开发环境。LAMP是一个缩写词&#xff0c;具体包括Linux操作系统、Apache网站服务器、MySQL数据库服务器、PHP&#xff08;或P…

samba-3.3.16 在海思平台(SS528)交叉编译及问题解决

目录 <center>一、概述<center>二、编译步骤&#x1f449;2.1 解压缩&#xff0c;进入源码目录&#x1f449;2.2 配置&#x1f449;2.3 问题一&#x1f449;2.4 问题二&#x1f449;2.5 问题三 一、概述 远程开关机功能需要用到samba里面的一个工具net&#xff0c;…

APP UI自动化测试框架总结,各种项目实战加源码等你来拿

目录 开发语言选择 UI测试框架选择 单元测试框架选择 测试环境搭建 脚本编写 Jenkins集成 开发语言选择 通常用于自动化测试的编程语言有&#xff1a;Python、Java、Javascript、Ruby、C#、PHP等。一般我们会选择自己熟悉的编程语言来编写自动化脚本&#xff0c;但对于编…

spark安装

安装 su - root https://repo.anaconda.com/archive/ Anaconda3-2021.05-Linux-x86_64.sh sh ./Anaconda3-2021.05-Linux-x86_64.sh yes enter exit() exit() 重新登录 su - root 配置成功 (base) [rootnode1 ~]# python Python 3.8.8 (default, Apr 13 2021, 19:58:26) [GC…

SpringBoot集成slf4j日志和logback.xml配置详解

SpringBoot集成slf4j日志和logback.xml配置详解 一、Slf4j概述二、使用Slf4j打印日志1.代码方式使用Slf4j2.lombok使用注解引入Slf4j 三、项目如何配置logback.xml1. applicaiton.yml配置2. 日志级别及优先级3. logback.xml日志配置文件4. logback.xml日志配置的实现效果 四、l…

Coremail与中科曙光达成战略合作 紧抓数字经济大机遇

5月12日&#xff0c;广东盈世计算机科技有限公司&#xff08;以下简称&#xff1a;Coremail&#xff09;与曙光信息产业股份有限公司&#xff08;以下简称&#xff1a;中科曙光&#xff09;正式签约合作协议、达成战略合作伙伴关系。 Coremail技术副总裁林延中、中科曙光副总裁…

【高级语言程序设计(一)】第 10 章:文件

目录 一、文件概述 &#xff08;1&#xff09;文件定义 &#xff08;2&#xff09;文件命名 &#xff08;3&#xff09;文件分类 ① 按照文件的内容划分 ② 按照文件的组织形式划分 ③ 按照文件的存储形式划分 ④ 按照文件的存储介质划分 &#xff08;4&#xff09;文…

vulnhub dc-7

1.信息搜集 端口 22,80 存活主机 192.168.85.134 2.访问网站进行信息搜集 发现提示信息&#xff0c;大体意思&#xff0c;有一个新概念&#xff0c;爆破可能失败&#xff0c;跳出框思考 cms&#xff1a;drupal 8.0 msf尝试利用失败 3.对网站进行渗透测试 尝试弱口令 失败 试出…

MapReduce实现KNN算法分类推测鸢尾花种类

文章目录 代码地址一、KNN算法简介二、KNN算法示例&#xff1a;推测鸢尾花种类三、MapReduceHadoop实现KNN鸢尾花分类&#xff1a;1. 实现环境2.pom.xml 3.设计思路及代码1. KNN_Driver类2. MyData类3. KNN_Mapper类 4. KNN_Reducer类 代码地址 https://gitcode.net/m0_567453…