JaveEE:手动实现定时器精讲

news2024/12/23 18:13:20

前言

在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);
    }

}

运行结果:

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

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

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

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

相关文章

elementui + vue2实现表格行的上下移动

场景&#xff1a; 如上&#xff0c;要实现表格行的上下移动 实现&#xff1a; <el-dialogappend-to-bodytitle"条件编辑":visible.sync"dialogVisible"width"60%"><el-table :data"data1" border style"width: 100%&q…

mybatis高级扩展-插件和分页插件PageHelper

1、建库建表 create database mybatis-example; use mybatis-example; create table emp (empNo varchar(40),empName varchar(100),sal int,deptno varchar(10) ); insert into emp values(e001,张三,8000,d001); insert into emp values(e002,李四,9000,d001); insert into…

MATLAB代码:分布式电源接入对配电网影响分析

微♥关注“电击小子程高兴的MATLAB小屋”获取专属优惠 关键词&#xff1a;分布式电源 配电网 评估 仿真平台&#xff1a;MATLAB 主要内容&#xff1a;代码主要做的是分布式电源接入场景下对配电网运行影响的分析&#xff0c;其中&#xff0c;可以自己设置分布式电源接入…

【信息学奥赛】拼在起跑线上,想入道就别落下自己!

编程无难事&#xff0c;只怕有心人&#xff0c;学就是了&#xff01; 文章目录 1 信息学奥赛简介2 信息学竞赛的经验回顾3 优秀参考图书推荐《信息学奥赛一本通关》4 高质量技术圈开放 1 信息学奥赛简介 信息学奥赛&#xff0c;作为全国中学生学科奥林匹克“五大学科竞赛”之一…

LeetCode刷题--- 二叉树剪枝

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏&#xff1a;http://t.csdnimg.cn/ZxuNL http://t.csdnimg.cn/c9twt 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回溯算法&#xff0c;所以下面题目主要也是这些算法做的 我讲述…

分析若依的文件上传处理逻辑

分析若依的文件上传处理逻辑 注&#xff1a;已经从若依框架完成拆分&#xff0c;此处单独分析一下人家精彩的封装&#xff0c;也来理解一下怎么做一个通用的上传接口&#xff01;如有分析的&#xff0c;理解的不透彻的地方&#xff0c;大家多多包含&#xff0c;欢迎批评指正&am…

Linux Zabbix企业级监控平台本地部署并实现远程访问

前言 Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 本地zabbix web管理界面限制在只能局域…

数据结构 | DFSBFS,Prim代码

树的DFS&BFS prim算法 图的DFS和BFS DFS

Java小案例-RocketMQ的11种消息类型,你知道几种?(延迟消息)

前言 上一节给大家讲了Rocket的顺序消息&#xff0c;这一节和大家聊一下延迟消息&#xff0c;关于顺序消息大家可以点下面这个链接直接看 RocketMQ的延迟消息 延迟消息 延迟消息就是指生产者发送消息之后&#xff0c;消息不会立马被消费&#xff0c;而是等待一定的时间之后…

JMeter下载与安装

文章目录 前言一、安装java环境&#xff08;JDK下载与安装&#xff09;二、JMeter下载三、JMeter安装1.解压缩2.配置环境变量 四、JMeter启动&#xff08;启动成功则代表JMeter安装成功&#xff09;五、JMeter汉化&#xff08;将JMeter修改成中文&#xff09;1.方法一&#xff…

【Linux】内核结构

一、Linux内核结构介绍 Linux内核结构框图 二、图解Linux系统架构 三、驱动认知 1、为什么要学习写驱动2、文件名与设备号3、open函数打通上层到底层硬件的详细过程 四、Shell Shell脚本 一、Linux内核结构介绍 Linux 内核是操作系统的核心部分&#xff0c;它负责管理系…

【Android】MVC与MVP的区别,MVP网络请求实践

一、MVC模式 目录 一、MVC模式二、MVP模式 1、MVP的简单应用 1.1 导入相关依赖包并设置权限1.2 实现Model1.2 实现Presenter1.3 实现View1.4分析项目结构和绑定过程1.5效果展示 2、MVP结合RxJava 一、MVC模式 MVC&#xff08;Model(模型)——View(视图)——Controller(控制…

三层交换,DHCP的详解与VRRP

目录 一、三层交换 1、三层交换机的作用&#xff1a; 2.vlan的虚拟接口vlanif&#xff08;ifinterface接口&#xff09; 3.三层交换机实验 4.拓展实验​编辑 二、DHCP 1.自动获取ip地址&#xff1a; 2.DHCP的好处&#xff1a; 3.分配方式&#xff1a; 4.举例&#xff…

6.rk3588获取摄像头和激光雷达数据(用线程根据时间同步)

文件夹结构如下&#xff1a; 如果没有特殊说明&#xff0c;我们将py文件写在该路径里面。 保存数据的路径如下&#xff1a; ---img_lidar_save ---2023-12-13&#xff08;根据日期自动生成当天保存数据的文件夹) ---camera_data(相机数据文件夹&#xff09; ---image(保存相加…

C++1114新标准——模板模板参数(Template Template Parameter)、using

系列文章目录 C11&14新标准——Variadic templates&#xff08;数量不定的模板参数&#xff09; C11&14新标准——Uniform Initialization&#xff08;统一初始化&#xff09;、Initializer_list&#xff08;初始化列表&#xff09;、explicit C11&14新标准—— d…

SpringBoot 究竟是如何跑起来的

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《SpringBoot》。&#x1f3af;&#x1f3af; &…

怎么把文件转成附件放在公众号里?这篇教程给你详细说清楚

文件转附件&#xff0c;其实就是把文件上传到某个网站&#xff0c;获得文件的下载链接&#xff0c;从而放到文章或者其他地方供读者下载使用。因为公众号并不支持直接在文章里面添加下载链接&#xff08;至少订阅号不行&#xff09;&#xff0c;所以把文件转成下载链接的方式并…

怎么制作GIF动图?教你这几个简单方法

怎么制作gif动图&#xff1f;GIF动图是一种非常有趣且实用的图片格式&#xff0c;它能够以短小精悍的方式展示动画效果&#xff0c;因此在社交媒体和聊天应用中备受追捧。本文将向您介绍几种制作GIF动图的方法&#xff0c;让您轻松制作出自己的动图。 GIF动图制作方法一&#x…

Spark编程实验一:Spark和Hadoop的安装使用

一、目的与要求 1、掌握在Linux虚拟机中安装Hadoop和Spark的方法&#xff1b; 2、熟悉HDFS的基本使用方法&#xff1b; 3、掌握使用Spark访问本地文件和HDFS文件的方法。 二、实验内容 1、安装Hadoop和Spark 进入Linux系统&#xff0c;完成Hadoop伪分布式模式的安装。完成Ha…

【开源项目】智慧水厂—经典开源项目实景三维数字孪生智慧水厂

智慧水务可视化平台是以物联网IOT技术为核心&#xff0c;以数据库系统为支撑&#xff0c;以城市水资源安全提升和建造智能化为目标的智慧水务体系。飞渡科技利用数字孪生技术结合物联网IOT技术&#xff0c;建立起多个基础数据及管理层级矩阵&#xff0c;可以跨部门、跨层级进行…