定时器(Timer)

article/2025/10/11 0:06:11

在这里插入图片描述

一、定时器是什么?

定时器类似于我们生活中的闹钟,可以设定一个时间来提醒我们。
而定时器是指定一个时间去执行一个任务,让程序去代替人工准时操作。
标准库中的定时器: Timer

方法作用
void schedule(TimerTask task, long delay)指定delay时间之后(单位毫秒)执行任务task
public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("定时器任务! ");}},1000);}

这段程序就是创建一个定时器,然后提交一个1000s后执行的任务。

二、自定义定时器

我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:
1.一个扫描线程,负责来判断任务是否到时间需要执行
2.需要有一个数据结构来保存我们定时器中提交的任务

创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个Task对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityBlockingQueue比较合适。

首先我们构造一个Task对象

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

MyTimer类:

public class MyTimer {//扫描线程private Thread t;//创建一个阻塞优先级队列,用来保存提交的Task对象private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();private Object locker = new Object();//提交任务的方法public void schedule(Runnable runnable,long time) {//这里我们的时间换算一下,保存实际执行的时间MyTask task = new MyTask(runnable,System.currentTimeMillis() +  time);queue.put(task);}//构建扫描线程public MyTimer() {t = new Thread(() -> {//我们取出队列中时间最近的元素while (true) {try {MyTask task = queue.take();long curTime = System.currentTimeMillis();if(curTime < task.getTime()) {//证明还没到执行的时间,再放进队列queue.put(task);} else {//到时间了,执行任务task.run();}} catch (InterruptedException e) {e.printStackTrace();}}});}t.start();
}

虽然我们大体已经写出来了,但是我们这个定时器实现的还有一些问题。
问题1:既然我们是优先级队列,我们再阻塞优先级队列中放入Task对象时,是根据什么建立堆的?
在这里插入图片描述
我们发现当我们运行程序时,我们的程序也会报这样的错误。

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();}@Overridepublic int compareTo(MyTask o) {return (int) (this.time - o.time);}
}

我们需要实现Comparable接口并且重写compareTo方法,指明我们是根据时间来决定在队列中的优先级。

2.我们的扫描线程,扫描的速度太快,造成了不必要的CPU资源浪费。
在这里插入图片描述
比如我们早上8.00提交了一个中午12.00的任务,那么我们这样的程序就会从8.00一直循环几十亿次,而这样的等待是没有任何意义的。
更合理的方式是,不要在这里忙等,而是“阻塞式”等待。

public MyTimer() {t = new Thread(() -> {//我们取出队列中时间最近的元素while (true) {try {MyTask task = queue.take();long curTime = System.currentTimeMillis();if(curTime < task.getTime()) {//证明还没到执行的时间,再放进队列queue.put(task);synchronized (locker) {locker.wait(task.getTime() - curTime);}} else {//到时间了,执行任务task.run();}} catch (InterruptedException e) {e.printStackTrace();}}});}t.start();

我们重写一下扫描线程,进行修改,当我们判断队列中最近的一个任务的时间都没到时,我们的扫描线程就进行阻塞等待,这里我们使用的不是wait(),而是wait(long time),我们传入的参数是要执行的时间和当前时间的差值,有的同学可能会问了,那这样执行的时候和预期执行的时间不就有出入了嘛?
因为我们程序里的定时操作,本来就难以做到非常准确,因为操作系统调度是随机的,有一定的时间开销,存在ms的误差都是相当正常的,不影响我们的正常使用。
我们上面进行阻塞等待,难道就傻傻的等到时间到了自动唤醒嘛? 有没有啥特殊情况呢?这里是有的,比如我们设定了一个阻塞到12点在唤醒,但我们又提交了一个10点的新任务,那么我们就应该提前唤醒了,所以我们应该在每次提交任务后都进行主动唤醒,再由我们扫描线程决定是执行还是继续阻塞等待。

public void schedule(Runnable runnable,long time) {//这里我们的时间换算一下,保存实际执行的时间MyTask task = new MyTask(runnable,System.currentTimeMillis() +  time);queue.put(task);synchronized (locker) {locker.notify();}}

即使我们现在所有正常的情况都考虑到了,但是我们这里仍然存在一种极端的情况。
在这里插入图片描述

假设我们的扫描线程刚执行完put方法,这个线程就被cpu调度走了,此时我们的另一个线程调用了schedule,添加了新任务,新任务是10点执行,然后notify,因为我们并没有wait(),所以相当于这里是空的notify,然后我们的线程调度回来去执行wait()方法,但是我们的时间差仍然是之前算好的时间差,从8.00点到12.00点,这样就会产生很大的错误。
这里造成这样的问题,是因为我们的take操作和wait操作不是原子的,我们需要在take和wait之间加上锁,保证每次notify的时候,都在wait中。

public MyTimer() {t = new Thread(() -> {while (true) {try {synchronized (locker) {MyTask Task = queue.take();long curTime = System.currentTimeMillis();if (curTime < Task.getTime()) {queue.put(Task);locker.wait(Task.getTime() - curTime);} else {Task.run();}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();

在这里插入图片描述


http://chatgpt.dhexx.cn/article/1VBrYH51.shtml

相关文章

STM32-定时器详解

目录 前言 一、定时器基本介绍 1. STM32定时器 2. 通用定时器功能和特点 3. 计数器模式 4. 定时器工作原理 a.定时器框图 b.时钟产生器部分 c.时基单元 d.输入捕获通道 e.输出比较通道&#xff08;PWM&#xff09; 二、定时器中断应用 1.内部时钟选择 2.计数器模式 …

typedef和#define

typedef是c语言中一个重要的关键字其作用是为一种数据类型定义了一个新的名字这里的类型包括&#xff08;int&#xff0c;char,double 等)和自定义数据类型&#xff0c;通俗一点来说就是为一种数据类型起一个别名 举个例子&#xff1a; 定义一个整型变量a并将其初始化为666&a…

typedef和define的区别、typedef的具体用法

typedef最核心的用法&#xff1a;给数据类型取别名&#xff0c;这个别名既可以是此数据类型的替换&#xff0c;也是指向此数据类型的指针。 具体用法&#xff08;对普通数据类型取别名&#xff09;&#xff1a; 对结构体数据类型取别名&#xff1a; typedef与define的区别&…

#define与typedef的区别

目录 &#xff08;1&#xff09;原理不同 &#xff08;2&#xff09;功能不同 &#xff08;3&#xff09;作用域不同 &#xff08;4&#xff09;对指针的操作不同 &#xff08;5&#xff09;补充 a.指针常量 b.常量指针 typedef和define都是替一个对象取一个别名&#x…

C语言中的typedef

C语言中的"typedef" 一、什么是typedef typedef是用于定义新的类型名&#xff0c;在编程中可以用typedef来定义新的类型名来代替已有的类型名 格式&#xff1a; typedef 已有类型名 新的类型名 通俗点说&#xff0c;就是为已有的类型取别名&#xff0c;例如 老鼠&am…

define 与typedef的区别

define 与typedef大体功能都是使用时给一个对象取一个别名&#xff0c;增强程序的可读性&#xff0c;但它们在使用时有以下几点区别&#xff1a; 1.定义不一样 define定义后面不用加分号&#xff0c;并且它的别名在对象的前面 typedef需要加分号&#xff0c;并且它的别后面替…

C语言学习笔记---typedef 简介

在单片机和操作系统中 typedef 会经常用到&#xff0c;它可以为某一个类型自定义名称。和#define比较类似。但是又有不同的地方。 typedef 创建的符号只能用于数据类型&#xff0c;不能用于值。而#define 创建的符号可以用于值。typedef 是由编译器来解释&#xff0c;而不是预…

typedef介绍

[20210330更新]&#xff1a;这篇博客写的时间有点久了:)。本次更新修改了博客内容中的错误和表述不当的地方。 本文介绍C语言中的关键字 typedef 的用法。 1 概述 typedef 为C语言的关键字&#xff0c;作用是为一种数据类型定义一个新名字&#xff0c;这里的数据类型包括内部…

【C语言】typedef的使用

目录 一、什么是typedef 二、typedef用法 1、对于数据类型使用例如&#xff1a; 2、对于指针的使用例如 3、对于结构体的使用 三、进阶typedef 1、数组指针 2、指针函数 3、Int *(*array[3])(int); 4、Void (*funA(int,void(*funB)(int)))(int); 四、Typedef与defin…

ubuntu etc 设置权限777带来的问题

ubuntu etc 设置权限777带来的问题--sudoers权限错误 在一次工作中&#xff0c;在ubutu通过命令行输入 sudo chmod -R 777 /etc 命令&#xff0c;误将etc目录权限更改为了777&#xff08;rwxrwxrwx&#xff09; 导致服务器无法ssh远程连接&#xff0c;而且sudo命令无法使用 提…

Linux修改文件权限为777

将文件权限改为777的命令为chmod 777 文件名 777说明&#xff1a; 你可以在linux终端先输入ll,可以看到如: -rwx-r--r-- (一共10个参数) 第一个跟参数跟chmod无关,先不管. 2-4参数:属于user 5-7参数:属于group 8-10参数:属于others 接下来就简单了:r>可读 w>可写 x&…

文件权限777

一个文件的权限为777(linux中 ls -l xxx.xxx &#xff08;xxx.xxx是文件名,查看的是xxx文件之中的文件权限)) 这三个数字分别表示&#xff1a;不同用户或用户组的权限。 第一个数字表示文件所有者的权限 第二个数字 表示与文件所有者同属一个用户组的其他用户的权限 第三个数…

Linux-权限管理(你听过777、755、644吗)

文章目录 组rwx权限修改权限-chmod修改文件所有者-chown修改文件所在组-chgrp 组 linux 中每个文件有所有者、所在组、其它组的概念。 类似linux 中的每个用户必须属于一个组&#xff0c;不能独立于组外&#xff0c;组的相关操作可参考&#xff1a;Linux-用户管理 所有者 文件…

Linux文件属性的777权限

一、文件属性概述 Linux系统是一种典型的多用户系统&#xff0c;不同的用户处于不同的地位&#xff0c;拥有不同的权限。为了保护系统的安全性&#xff0c;Linux系统对不同的用户访问同一文件&#xff08;包括目录文件&#xff09;的权限做了不同的规定。在Linux中我们可以使用…

Linux的777权限

Linux的每个文件一般都有三个权限 r–读&#xff0c;w–写&#xff0c;x–执行&#xff0c;其分别对应的数值为4&#xff0c;2&#xff0c;1。 输入ll可以查看到文件的权限。 聪明的同学已经看出来了——7即代表有读、写和执行的权限。那么为什么是三个7呢&#xff1f;“三”是…

Linux 777 权限表示什么,各数字又是什么含义?

最近在面试中&#xff0c;问到了 Linux 777 权限表示什么&#xff0c;各数字的含义又是什么。小格子通过自己的理解和查找的资料&#xff0c;做了如下总结&#xff0c;希望读者们遇到此问题时&#xff0c;可以快速又正确的回答。 1、三种权限 Linux 下的每个文件都有以下三种…

C语言移位运算(<<)

上来个题目给大家感受下 解题 #include<stdio.h> int main() {int x;while(scanf("%d",&x)!EOF){printf("%d\n",1<<x);}return 0; } 输出结果 其他&#xff1a; 运算符含义 运算符含义描述<<左移二进制数左移x位&#xff0c;高…

为什么我的C语言移位操作达不到效果??

为什么我的C语言移位操作达不到效果&#xff1f;&#xff1f;&#x1f632;&#x1f632; 今天下午在对蓝牙数据处理的时候发现&#xff1a; wrwr<<8read_Buffer[2];write(44,wr,0); 达不到预期效果&#xff1b;但把上述代码改为wrwr*256read_Buffer[2];write(44,wr,0…

C语言之移位操作符、位操作符详解

目录 1、移位操作符 2、位操作符 1、移位操作符 分为左移操作符&#xff08;<<&#xff09;和右移操作符(>>)。 左移操作符&#xff1a; 移位规则&#xff1a;左边抛弃&#xff0c;右边补0。 什么意思呢&#xff0c;现在就让我来仔细讲解一番。 比如我们定义…

整数乘法c语言移位实现,C语言 用移位、异或、与运算实现加法

C语言 用移位、异或、与运算实现加法 一、说明 计算机整数的加减乘除就是依靠位运算实现的。 比如整数的运算&#xff1a;加法&#xff1a;通过异或、与、移位实现&#xff1b;减法&#xff1a;a-b其实就是a(-b);乘法&#xff1a;5*3其实就是555&#xff1b;除法&#xff1a;7/…