当前位置: 首页 > news >正文

【JavaEE】定时器

目录

前言

什么是定时器

如何使用java中的定时器

实现计时器

实现MyTimeTask类

Time类中存储任务的数据结构

实现Timer中的schedule方法

 实现MyTimer中的构造方法

处理构造方法中出现的线程安全问题

完整代码

 考虑在限时等待wait中能否用sleep替换

能否用PriorityBlockingQueue进行存储


在前面,已经讲解了几种常见设计模式,那么今天我们就来讲解一下定时器。

前言

在发送信息的时候,有时候不想要信息那么快就发送出去,而是在特定的时间再发送;或者我们在发送邮件时,当达到特定的时间时,就会自动发送电子邮件给用户,那么这里就需要用到定时器,那么定时器是什么呢?

什么是定时器

定时器是软件开发中的一个重要组件,类似于“闹钟”,能够在某个特定的时间执行一个或者多个任务,定时器是多线程中的一个案例,也是一个比较复杂且重要的案例。

如何使用java中的定时器

在java中,给我们提供了实现了的定时器包,我们可以直接使用。

java中给我们提供的定时器是Timer,我们在设置定时任务时,需要用到其中的schedule方法。

schedule包含两个参数:

第⼀个参数指定即将要执⾏的任务代码

第⼆个参数指定多⻓时间之后 执⾏(单位为毫秒)

示例:

class Demos{public static void main(String[] args) {// 创建一个Timer对象,用于调度定时任务Timer timer=new Timer();// 调度第一个定时任务,1秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},1000);// 调度第二个定时任务,2秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},2000);// 调度第三个定时任务,3秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},3000);}}

实现计时器

我们从上述代码中可以看出,要实现一个定时器,需要实现以下:

  1. 实现一个任务(Task)类
  2. 实现一个Timer类用来存放任务

实现MyTimeTask类

/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay    任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long  delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}
}

Time类中存储任务的数据结构

我们在存储任务的时候,需要根据等待时间来存储,时间短的优先取出来,那么我们就可以使用优先级队列,创建一个小根堆,时间最短的放在堆顶。

    private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>();

但是我们这里要怎么比较呢?我们可以实现Comparable接口重写compareTo方法来进行比较,或者创建一个类来实现Comparator接口重写compare方法来进行比较。

实现Timer中的schedule方法

当我们解决了在任务在优先级队列中如何进行比较存储任务的问题之后,那么就可以在MyTimer中来实现schedule方法。根据schedule方法的参数创建一个MyTimeTask类,并将其添加到优先级队列中。

/*** 将一个 Runnable 任务安排在指定的延迟时间后执行* * @param runnable 要执行的任务* @param delay    相对于现在的时间延迟,单位为毫秒* * 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/
public void schedule(Runnable runnable, long delay) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);
}

 实现MyTimer中的构造方法

通过实例化一个线程,在这个线程中,通过多次扫描优先级队列中的元素,判断堆顶元素是否到达了等待时长,若是,则取出并执行。

注意:这里不能直接poll取出栈顶元素,若栈顶的任务等待时间还未到达,则继续循环。

 /*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {if(pq.isEmpty()){continue;}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();}else {// 如果栈顶的任务时间大于当前时间,则继续循环// 继续检查下一个任务,或在没有到达时间的任务时继续等待continue;}}});// 启动线程t.start();}

处理构造方法中出现的线程安全问题

在上述代码中,能看出哪里存在线程安全问题吗?

优先级队列并不是一个线程安全的队列,我们在瞥(peek)取(poll)的时候,可能会出现线程安全问题,若是在多线程环境中,当线程1刚peek了堆顶任务,但此时切换到线程2,线程2同样peek堆顶任务,并刚好到了等待时间,此时就会执行并且删除栈顶任务。此时又切换到线程1,但此时线程1peek的堆顶任务已经被poll掉了,此时如果再执行,就会再次删除堆顶任务,导致出现线程安全问题。

所以,这里我们需要对peek和poll操作进行加锁。

    /*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环if (pq.isEmpty()) {continue;}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环// 继续检查下一个任务,或在没有到达时间的任务时继续等待continue;}}}});// 启动线程t.start();}

 这里还有什么能优化的吗?

我们可以看到,当优先级队列中不为空,但此时堆顶任务的等待时间还没到,此时就会进入else分支执行continue,但一直重复这样操作,可能会造成不断检查,cpu使用率过高。那么我们就可以使用带参数的wait来进行限时等待,当达到时限时,会自动唤醒线程。

同理的,在判断队列是否为空时,我们可以设置不带参数的wait,等待唤醒。

    /*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}

既然这里等待,那么我们就需要有人来唤醒wait,所以我们在schedule方法中也需要进行加锁,并且在添加完任务后,调用notify来进行通知。

/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay    相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}

 MyTimer优化到这里,其实已经优化好了。

完整代码

package Threads;import java.util.Comparator;
import java.util.PriorityQueue;/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask implements Comparable<TimeTask>{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay    任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long  delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}@Overridepublic int compareTo(TimeTask o) {return (int) (this.time- o.getTime());}
}
class MyTimer{//private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>(new compareTimeTask());private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>();static Object lock=new Object();/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay    相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}}
class Demo{public static void main(String[] args) {MyTimer timer=new MyTimer();timer.schedule(()->{System.out.println("Hello World1");},1000);timer.schedule(()->{System.out.println("Hello World2");},2000);timer.schedule(()->{System.out.println("Hello World3");},3000);}
}
class compareTimeTask implements Comparator<MyTimeTask>{@Overridepublic int compare(MyTimeTask o1, MyTimeTask o2) {return (int) (o1.getTime()-o2.getTime());}
}

测试一下

 考虑在限时等待wait中能否用sleep替换

 Thread.sleep(task.getTime()-System.currentTimeMillis());

 这里为什么不用sleep呢?

在前面线程安全问题中已经讲解了wait和sleep的区别,在这里,如果我们使用sleep,会导致拉着锁一起进入睡眠,导致其他线程拿不到锁对象,无法进行加锁。

这会导致我们想要调用schedule方法添加任务时,拿不到锁对象。

能否用PriorityBlockingQueue进行存储

在前面,我们用的是优先级队列PriorityBlockingQueue来存储任务,但如果我们用PriorityBlockingQueue呢?

如果我们使用PriorityBlockingQueue,那么我们的方法也需要改成take(取)和put(存),才能用阻塞等待的效果。

修改代码:

package Threads;import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask implements Comparable<TimeTask>{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay    任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long  delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}@Overridepublic int compareTo(TimeTask o) {return (int) (this.time- o.getTime());}
}
class MyTimer{//private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>(new compareTimeTask());private PriorityBlockingQueue<MyTimeTask> pq=new PriorityBlockingQueue<>(100);static Object lock=new Object();/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = null;try {task = pq.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环pq.put(task);try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay    相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.put(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}}
class Demo{public static void main(String[] args) {MyTimer timer=new MyTimer();timer.schedule(()->{System.out.println("Hello World1");},1000);timer.schedule(()->{System.out.println("Hello World2");},2000);timer.schedule(()->{System.out.println("Hello World3");},3000);}
}
class compareTimeTask implements Comparator<MyTimeTask>{@Overridepublic int compare(MyTimeTask o1, MyTimeTask o2) {return (int) (o1.getTime()-o2.getTime());}
}

由于take会触发阻塞等待,而后面的wait也会,这里加了两次锁,容易引出线程安全问题,所以我们建议使用一个锁对象lock,来进行加锁就行。而不使用无界阻塞队列。


以上就是本篇所有内容~

若有不足,欢迎指正~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • MySQL基础操作指南:从连接到数据管理的完整指南
  • Excel表列名称
  • React类组件与函数组件有什么异同
  • Linux IPC-管道
  • 探索AI与社交的交汇点:看Facebook如何引领智能化革命
  • SSM伊犁旅游攻略网站—计算机毕业设计源码15961
  • 简短而精辟: 用什么样的约束思路能提高 (LLM) 成绩?
  • spring boot3.x快速入门
  • 代码规范 —— 数据库规范
  • 麦田物语第二十天
  • JVM知识总结(CMS收集器)
  • Element学习(表格组件、分页组件)(2)
  • openwrt编译Dockerfile
  • TV-L1光流算法流程
  • C++ SQL ORM
  • [deviceone开发]-do_Webview的基本示例
  • hadoop集群管理系统搭建规划说明
  • javascript面向对象之创建对象
  • JS变量作用域
  • js算法-归并排序(merge_sort)
  • node.js
  • PHP 小技巧
  • Python连接Oracle
  • SpringBoot 实战 (三) | 配置文件详解
  • Sublime Text 2/3 绑定Eclipse快捷键
  • vue-loader 源码解析系列之 selector
  • 阿里研究院入选中国企业智库系统影响力榜
  • 批量截取pdf文件
  • 深度学习中的信息论知识详解
  • 首页查询功能的一次实现过程
  • 提醒我喝水chrome插件开发指南
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • 进程与线程(三)——进程/线程间通信
  • ​io --- 处理流的核心工具​
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • ###STL(标准模板库)
  • (4)事件处理——(7)简单事件(Simple events)
  • (70min)字节暑假实习二面(已挂)
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (二)丶RabbitMQ的六大核心
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (四)js前端开发中设计模式之工厂方法模式
  • (四)库存超卖案例实战——优化redis分布式锁
  • (学习日记)2024.02.29:UCOSIII第二节
  • (一)RocketMQ初步认识
  • (转)Oracle 9i 数据库设计指引全集(1)
  • (转)创业家杂志:UCWEB天使第一步
  • (转)关于pipe()的详细解析
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • (转)平衡树