【多线程案例】Java实现简单定时器(Timer)

news2024/11/16 2:53:17

1.定时器(Timer)

1.什么是定时器?

在日常生活中,如果我们想要在 t 时间 后去做一件重要的事情,那么为了防止忘记,我们就可以使用闹钟的计时器功能,它会在 t 时间后执行任务(响铃)提醒我们去执行这件事情. — 这就是Java定时器的简单功能。它作为一种日常开发组件。约定一个时间,时间到达之后,执行某个任务。常被用于网络通信。

也比如在客户端和服务器之间,当客户端发出去请求之后,服务器就要返回响应,客户端这边要等待响应,而网络环境是复杂的,如果等待时间较长,这个原因是啥,是请求没法送过去?响应丢了?还是服务器出问题了。对于客户端来说,不能无限的等,需要先设置一个最大的期限。这时"等待最大期限"就可以通过定时器的方式实现了。

2. Java内置定时器的常用功能

Java中的定时器的类是 : Timer ,为util包中的一个无继承关系的类, 从该类的构造方法中,我们可以使用无参构造器创建该类的对象,也可以在创建类对象的时候指定定时器中所需要的线程的名字,与是否为守护进程。

  • 在定时器中最常用的方法就是 schedule(TimerTask task, long delay)
  • 该方法传参的是一个 TimerTask 对象,与定时器约定的执行时间间隔 delay
  • 包:import java.util.Timer;

 而 TimerTask 类则是一个来描述计时器任务的类,该类中有 抽象方法 run(),并且该类是实现了runnable接口的,所以我们给 schedule 传参中的 TimerTask 对象都要重写 run() 方法,重写的run() 方法中的语句,则是定时器需要执行的语句。

而 delay 则是我们约定从当前时间后的 delay 内执行传入的任务.时间单位为 毫秒。 

 接下来我们来看一个简单的定时器的使用 :

我们在创建定时器的时候指定了定时器中的扫描线程的线程名,然后使用 schedule 方法传入任务与任务执行的间隔时间 1000 毫秒
这个时候在执行该代码的1000毫秒后,定时器就会将该任务执行。

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

public class demo1 {
    public static void main(String[] args) {
        Timer timer = new Timer("线程1");
        timer.schedule(new TimerTask() {   //使用匿名内部类继承TImerTask类
            @Override
            public void run() {    //TimerTask类实现了Runnable接口要重写run方法
                System.out.println("执行任务");
            }
        },1000);   //delay相对时间 任务执行时间

        System.out.println("程序启动!");
    }
}

主线程执行schedule方法的时候,就是把这个任务放到timer对象中了。并且timer里面也包含一个线程(扫描线程),时间一到,扫描线程就会执行刚才安排的任务了。

可以发现,程序运行完,整个程序并没有结束。正是因为TImer里的线程,阻止了线程结束!

利用 jconsole 观察该线程处于 WAITING 状态:

3.自定义定时器


3.1 实现定时器思路

  1. 实现定时器,我们首先需要有一个可以来描述定时器中的任务的类 MyTimerTask
  2. 需要使用一个数据结构将定时器中的任务按照执行时间的顺序给组织起来。
  3. 在 MyTimer 定时器类中会有一个线程不断地去访问定时器的任务,查看是否到了指定执行时间。
     

3.2 MyTimerTask 类:

//描述一个任务的类
public class MyTimerTask implements Comparable<MyTimerTask>{
    //要有一个任务
    private Runnable runnable;
    //要有一个时间
    private long time;

    //构造方法 传入任务和时间
    public MyTimerTask(Runnable runnable,long delay) {
        //任务
        this.runnable = runnable;
        //任务发生时间
        this.time = System.currentTimeMillis()+delay;
    }

    //为外部提供获取任务发生时间
    public long getTaskTime() {
        return this.time;
    }

    //为外部提供获取任务
    public Runnable getRunnable() {
        return this.runnable;
    }

    //重写比较方法
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time-o.time);
    }
}

实现Comparable接口是因为数据结构我们用到了优先级队列,需要重写比较方法,重新定义比较规则。有两种方法,一种是实现Comparable接口,另一种是比较器Comparator接口,用内部类实现。

3.3 MyTimer 类:

import java.util.PriorityQueue;

//定时器 即指定几分钟后或其他时间后干什么
//定时器
public class MyTimer {

    //优先级队列 使用比较器 匿名内部类
    //private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
    //    @Override
    //    public int compare(MyTimerTask o1, MyTimerTask o2) {
    //        return (int) (o1.getTaskTime()-o2.getTaskTime());
    //    }
    //});

    //优先级队列
    private  PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //多个线程针对同一个对象上锁  锁对象
    private Object locker = new Object();
    public void schedule(Runnable runnable,long delay) {
        //线程不安全
        synchronized (locker) {
            //添加任务及任务时间
            queue.offer(new MyTimerTask(runnable,delay));
            //唤醒队列
            locker.notify();
        }
    }

    //扫描线程
    public MyTimer() {
        //创建一个扫描线程
        Thread t1 = new Thread(()->{
            //不停地扫描队列 即队头 查看是否到达时间
            //有可能下一次新添加的任务的时间更短 队头改变
            while(true) {
                synchronized (locker) {
                    while (queue.isEmpty()) {
                        try {
                            //队列为空 等待
                            locker.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //队列不为空 有任务 下面进行时间比较

                    //获取当前任务
                    MyTimerTask task = queue.peek();

                    //获取当前时间  时间戳
                    long currTime = System.currentTimeMillis();

                    if(currTime>=task.getTaskTime()) {
                        //到任务时间  执行任务
                        task.getRunnable().run();
                        queue.poll();
                    }else{
                        //未到任务时间 也进行等待 降低扫描速度
                        try {
                            locker.wait(task.getTaskTime()-currTime);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }
        });

        //启动线程
        t1.start();
    }

}

数据结构我们选择使用优先级队列,有啥好处?假如在选择数据结构之前,我们先假设使用数组ArrayList,此时扫描线程,就需要不停地遍历数组中的每个任务,判定每个任务是否到达执行时间。这样的遍历效率是非常低的。如果使用优先级队列,再重写比较方法,让整个任务由时间大小按照小根堆排列,那么最先执行的就是时间最小的任务了,时间复杂度将降到O(1),判定任务时间是否到达更高效。

在该类的构造方法中,我们还创建了一个线程,不断地对任务队列中优先级最高(最快执行)的任务进行查看, 看是否到达执行时间。

当队列为空时,线程进入阻塞等待,直到添加一个任务时,线程继续执行。

队列中有任务时,但当前时间最短的任务还未到达执行时间时,也进行阻塞等待。这里的阻塞等待是有参的,为执行时间与当前时间的差值。其实也完全可以不等待,继续循环扫描。此处阻塞的好处就是wait之后,就会释放锁,线程就不会在CPU上执行了,就可以把CPU资源让给其他线程使用了。

那么对于wait和sleep来说都是等待,为啥不用sleep?sleep是指定时间让线程进行休眠,假如在sleep的过程中,我添加了一个比之前队列中任务执行时间还早的任务,那么sleep就不能及时执行最新的这个任务。而我设计的代码中,若使用wait,每添加一个任务时,notify都会唤醒不管是因为空队列进入阻塞状态的线程,或者是因为未到达任务时间而阻塞等待的线程(两种阻塞不会同时出现),就算是添加了一个比之前队列中任务执行时间还早的任务,也能及时执行任务。

3.4 测试

public class test {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        },1000);
        //System.out.println("00");

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        },2000);


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

结果: 

3.5 总结

三个注意点:

  1. 任务类,可比较的问题。
  2. 线程安全问题。
  3. 忙等问题。

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

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

相关文章

【数据结构-队列 二】【单调队列】滑动窗口最大值

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【单调队列】&#xff0c;使用【队列】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…

Springboot整合Druid:数据库密码加密的实现

ps:Springboot项目&#xff0c;为了防止某些人反编译看到yml里面的数据库密码&#xff0c;对密码进行加密处理&#xff0c;隐藏公钥形式。&#xff08;总有人想扒掉你的底裤看看你屁股长什么样&#xff09; 1.引入依赖&#xff08;以前有依赖就不用了&#xff09; 2.找到Druid…

你想知道的测试自动化-概览篇

测试自动化概念整理 协议 JSON Wire Protocol Specification JSON Wire 协议 现已过时的开源协议的端点和有效负载&#xff0c;它是W3C webdriver的先驱。 devtool协议 Chrome DevTools 协议允许使用工具来检测、检查、调试和分析 Chromium、Chrome 和其他基于 Blink 的浏…

轻松驾驭Hive数仓,数据分析从未如此简单!

1 前言 先通过SparkSession read API从分布式文件系统创建DataFrame 然后&#xff0c;创建临时表并使用SQL或直接使用DataFrame API&#xff0c;进行数据转换、过滤、聚合等操作 最后&#xff0c;再用SparkSession的write API把计算结果写回分布式文件系统 直接与文件系统交…

MyLife - Docker安装Redis

Docker安装Redis 个人觉得像reids之类的基础设施在线上环境直接物理机安装使用可能会好些。但是在开发测试环境用docker容器还是比较方便的。这里学习下docker安装redis使用。 1. Redis 镜像库地址 Redis 镜像库地址&#xff1a;https://hub.docker.com/_/redis/tags 这里是官方…

四向穿梭车智能机器人|HEGERLS托盘式四向穿梭车系统的换轨技术和故障恢复功能

随着物流行业的迅猛发展&#xff0c;托盘四向穿梭式立体库因其在流通仓储体系中所具有的高效密集存储功能优势、运作成本优势与系统化智能化管理优势&#xff0c;已发展为仓储物流的主流形式之一。托盘四向穿梭车立体仓库有全自动和半自动两种工作模式&#xff0c;大大提高了货…

java基础 异常

异常概述&#xff1a; try{ } catch{ }&#xff1a; package daysreplace;import com.sun.jdi.IntegerValue;import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.…

pymoo包NSGA2算法实现多目标遗传算法调参详细说明

pymoo包NSGA2算法实现多目标遗传算法调参详细说明 1.定义待求解问题1.0定义问题的参数说明1.0.0 求解问题必须设置在def _evaluate(self, x, out, *args, **kwargs)函数中1.0.1 问题必须用 out["F"] [f1, f2] 包裹起来1.0.2 约束条件也必须用 out["G"] […

Oracle 简介与 Docker Compose部署

最近&#xff0c;我翻阅了在之前公司工作时的笔记&#xff0c;偶然发现了一些有关数据库的记录。当初&#xff0c;我们的项目一开始采用的是 Oracle 数据库&#xff0c;但随着项目需求的变化&#xff0c;我们不得不转向使用 SQL Server。值得一提的是&#xff0c;公司之前采用的…

Windows保姆级安装Docker教程

1.官网下载 2.安装 3.启动Hyper-V 4.检查是否安装成功 1.下载 1.1.打开官网&#xff0c;然后点击下载 官网链接&#xff1a;https://hub.docker.com/ 2.安装 下载好之后会得到一个exe程序&#xff0c;然后启动它&#xff0c;进行安装。 去掉 WSL 不使用Hyper-V&#xff0…

KdMapper扩展实现之REALiX(hwinfo64a.sys)

1.背景 KdMapper是一个利用intel的驱动漏洞可以无痕的加载未经签名的驱动&#xff0c;本文是利用其它漏洞&#xff08;参考《【转载】利用签名驱动漏洞加载未签名驱动》&#xff09;做相应的修改以实现类似功能。需要大家对KdMapper的代码有一定了解。 2.驱动信息 驱动名称hwin…

使用testMe自动生成单元测试用例

文章目录 1、testMe简介2、插件对比2.1 testMe2.2 Squaretest2.3 Diffblue 3、IDEA插件安装4、单测用例4.1 maven依赖4.2 生成用例 5、自定义模板6、使用自定义模板生成用例7、调试用例 1、testMe简介 公司对于系统单元测试覆盖率有要求&#xff0c;需要达到50%或80%以上才可以…

RV1126-RV1109-进入uboot的按键和名字显示-HOSTNAME

今天添加一个小功能,就是uboot是按CTRLC进入的 今日我做了一个定制,让按L或者l让也进入uboot指令模式,并且修改主板名字显示 默认是CTRLC:键码值是0x03(ASCII对照表) 于是代码中跟踪: //rv1126_rv1109/u-boot/common/console.c int ctrlc(void) { #ifndef CONFIG_SANDBOXif (…

Python大数据之Python进阶(五)线程

文章目录 线程1. 线程的介绍2. 线程的概念3. 线程的作用4. 小结 线程 学习目标 能够知道线程的作用 1. 线程的介绍 在Python中&#xff0c;想要实现多任务除了使用进程&#xff0c;还可以使用线程来完成&#xff0c;线程是实现多任务的另外一种方式。 2. 线程的概念 线程是进程…

【chrome基础】Chrome、Chromium、libcef、electron版本关系大揭秘!

文章目录 概述chrome、Chromium、cef、electron 版本管理chrome的各种概念和学习资料V8 bindings 设计谷歌V8引擎探秘&#xff1a;基础概念Chrome 的插件&#xff08;Plugin&#xff09;与扩展&#xff08;Extension&#xff09;Chrome插件开发 概述 Chrome、Chromium、libcef、…

电荷泵CP原理及在PLL/DLL中的使用

参考【模拟集成电路】电荷泵&#xff08;CP&#xff09;设计_pll 电荷泵-CSDN博客 PLL-CP | Fitzs Blog 1.PLL/DLL中电荷泵概念及原理 电荷泵CP(charge pump)是锁相环中重要的一个模块&#xff0c;其主要功能是将鉴频鉴相器 (PFD) 输出的时钟相位差值转化为电荷&#xff0c;将…

UE5如何实现语言本地化管理(中英文切换)

一。实现蓝图的本地化控制 1.打开本地化控制面版 2.设置收集文本的路径 3.添加自己需要使用的语言&#xff0c;一般是中文 4.收集文本并进行转换语言的翻译 5.进入面板之后开始翻译 6.翻译完成之后计算字数并编译 7。一整套流程下来就是这样了 8.编译完成之后会在文件中生成…

一文汇总 Linux 内核调试的方法

内核开发比用户空间开发更难的一个因素就是内核调试艰难。内核错误往往会导致系统宕机&#xff0c;很难保留出错时的现场。调试内核的关键在于你的对内核的深刻理解。 在调试一个bug之前&#xff0c;我们所要做的准备工作有&#xff1a; 有一个被确认的bug&#xff0c;包含这…

10.10泊松、指数、伽马分布的理解

泊松定理、泊松分布 泊松定理就是描述在T段时间内每时每刻一直进行一个实验&#xff0c;这个实验成功的概率由t段时间总的成功期望次数决定&#xff0c;就是给二项分布加了个时间 泊松分布可用来描述某段时间来电次数的分布&#xff0c;电话台收到的呼叫数&#xff0c;商城的…

2023年【陕西省安全员B证】最新解析及陕西省安全员B证操作证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年陕西省安全员B证最新解析为正在备考陕西省安全员B证操作证的学员准备的理论考试专题&#xff0c;每个月更新的陕西省安全员B证操作证考试祝您顺利通过陕西省安全员B证考试。 1、【多选题】《陕西省建设工程质量…