多线程 - 定时器

news2024/11/20 3:31:05

v2-fba4ff60d8ab6684550fa59adcbee98b_b

多线程 - 定时器

定时器的背景知识

定时器 ~~ (就类似于定闹钟)

平时的闹钟,有两种风格:

  1. 指定特定时刻,提醒
  2. 指定特定时间段之后,提醒

这里的“定时器”,不是提醒,而是执行一个实现准备好的方法/代码,它是开发中一个常用的组件,尤其是在网络编程的时候,使用浏览器上网,打开一个网页,很容易出现,“卡了""连不上"的情况.这时就可以使用“定时器”来进行“止损”.

标准库提供的定时器

timer.schedule();这个方法的效果是,给定时器,注册一个任务.任务不会立即执行,而是在指定时间进行执行.

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("运行定时器任务");
    	}
	},3000);
}

第一个参数: new TimerTask() => TimerTask这个抽象类实现了Runnable接口,即将要执行的任务代码 ~~ public abstract class TimerTask implements Runnable
第二个参数: 指定多长时间之后执行(单位为毫秒)

手动实现一个定时器

定时器要求:

  1. 让被注册的任务,能够在指定时间被执行.
  2. 一个定时器是可以注册N个任务的,N个任务会按照最初约定的时间,按顺序执行.

思路:

在指定时间被执行 => 单独在定时器内部,创建个线程,让这个线程周期性的扫描,判定任务是否是到时间了.如果到时间了,就执行.没到时间,就再等等.
注册N个任务 => 这个N个任务,就需要使用一个数据结构来保存的,而在当下场景中,使用优先级队列,就是一个很好的选择.再由于这里的每个任务都是需要按时间执行的,时间越靠前,就越先执行,时间小的,优先级就高.此时队首元素,就是整个队列中,最先要执行的任务 => 这时,扫描线程,只需要扫一下队首元素即可,就不必遍历整个队列(如果队首元素还没到执行时间内,后续元素更不可能到时间).

image-20231006160206998

问题:

问题一: 因为调用schedule是一个线程,扫描是另一个线程,这里的优先级队列就会在多线程环境下使用了,这时就不得不考虑线程安全了.
问题二: 队列中的任务如何表示? 使用Runnable来表示任务的话是不行的,Runnable只是表述了任务内容,还需要描述任务什么时候被执行.
问题三: 如何进行任务的注册/创建?
问题四: 扫描线程具体的实现?
问题五: 任务MyTask如何进行优先级的比较?

解决:

问题一: 使用标准库提供的带优先级的阻塞队列 PriorityBlockingQueue,它本身就是线程安全的,就不需要考虑了.
问题二: 自定义一个MyTask类,来表示一个定时器中的任务,这个类包含两个私有属性private Runnable runnable;private long time; ~~ runnable是要执行的任务内容,time是任务在什么时候执行(使用毫秒时间来表示).
问题三: 提供一个schedule方法,来进行任务的注册/创建,这个schedule方法本身是比较的简单的,只是单纯的把任务放到队列里.
问题四: 取出队首元素, 检查看看队首元素任务是否是到时间了,如果时间没到,把取出来的元素重新入队queue.put(myTask);,在 put 之后, 再进行一个 waitthis.wait(myTask.getTime() - curTime);,如果时间到了,就执行任务内容.
问题五: 1.明确当前的任务是怎样的优先级,以哪个字段/属性指定优先级关系.2.让MyTask类实现Comparable接口,或者使用Comparator单独写个比较器(博主选择的是实现Comparable接口).

优化: Timer 类中存在一个 worker 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务设定的时间已经到达了,相关代码如下:

while (true) {     
	try {
		synchronized (this) {
			MyTask myTask = queue.take();
  			long curTime = System.currentTimeMillis();
  			if (curTime < myTask.getTime()) {
  				// 还没到时间,先不必执行
   				queue.put(myTask);
 			} else {
		 		// 时间到了,执行任务
 				myTask.run();
  			}
 		}
  } catch (InterruptedException e) {
      throw new RuntimeException(e);
   }
}

但是当前这个代码中存在一个严重的问题, 就是 while (true) {queue.put(myTask);}假设现在是8:00,队首元素的任务是10:00,取出的元素,显然是不能执行的,而由于这里的队列是优先级队列(堆),queue.put(myTask)会触发优先级调整,(堆的调整)调整之后, myTask 又回到队首了,下次循环取出来的还是这个任务. => 它就是一个没有任何阻塞的循环,在8:00到10:00这个时间段内,这个循环可能就要执行数以十亿次….就会造成了无意义的CPU浪费.

理解: 好比我们上高中的时间,每天都要6:00起床,而我有次5:00就醒了,看了眼闹钟,发现是5:00,正常来说,我会立刻继续睡,再睡个半小时,但是这个代码却不是这样的,按着这个代码执行逻辑的,我就必须在放下表后,又立刻拿起表来,又看时间,发现是5:00,然后又拿起闹钟,看时间,就这样重复着,知道时间到了6:00,然后才起床上学,但是这个一看不科学啊!这样做,就毫无意义,这样的代码是存在问题滴!!!

这种现象,在我们计算机领域也被称为“忙等” ~~ 等,但并没有闲着.正常来说,等待是要释放CPU资源的,让CPU做其它的事情,但是“忙等”,既进行了等待,又占用着CPU资源.
注: 像忙等这样的情况,也是需要辩证的看待的.在当前场景中,”忙等”,确实是不太好的.但是有的情况下,忙等,却是一个好的选择.

策略: 针对上述代码,就不要进行“忙等”了,而是进行"阻塞式"等待.这时就想到sleep或者wait,不过,博主要说的是sleep看似可行,但是实际上不可以的,因为做不到等待的时间明确!!!随时都可能会有新的任务创建/注册(随时可能有线程调用schedule添加新任务),万一新的任务更早了,是做不到等待时间的更新,此时仍然按照之前的等待,就会错过新任务的执行时间. 使用wait更合适,更方便随时唤醒.使用wait等待,每次有新任务来了(有线程调用schedule),就 notify一下,重新检查下时间.并再次计算要等待的时间,从而做到等待时间的更新.
注: 这里的wait是要使用带有“超时时间”版本的,这样就可以保证: 1.当新任务来了,随时 notify 唤醒; 2.如果没有新任务,则最多等到之前旧任务中的最早任务时间到,就被唤醒.


高能烧脑预警

博主代码写的过程中,遇到的一个线程安全/随机调度密切相关的问题.
考虑一个极端情况:
image-20231006221842050

看了上述图示之后,就不难发现,问题出现的原因,是因为当前 take 操作,和 wait 操作,并非是原子的.如果在 take 和 wait 之间加上锁,保证在这个过程中,不会有新的任务过来,问题自然解决(换句话说,只要保证每次 notify 时,确实都正在wait ) => 扩大上述代码锁的范围.

image-20231006223301553

代码编写:

package thread;


import java.util.concurrent.PriorityBlockingQueue;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: fly(逐梦者)
 * Date: 2023-10-06
 * Time: 16:32
 */

// 使用这个类来表示一个定时器的任务.
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;

    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                // 取出队首元素, 检查看看队首元素任务是否是到时间了
                // 如果时间没到,把取出来的元素重新入队
                // 如果时间到了,就把任务进行执行
                try {
                    synchronized (this) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime < myTask.getTime()) {
                            // 还没到时间,先不必执行
                            // 现在是13:00,取出来的任务是14:00 执行
                            queue.put(myTask);
                            // 在 put 之后, 再进行一个 wait
                            this.wait(myTask.getTime() - curTime);
                        } else {
                            // 时间到了,执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
    }

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

    // 指定两个参数
    // 第一个参数是 任务内容
    // 第二个参数是 任务在多少毫米之后执行. 形如 1000
    public void schedule(Runnable runnable, long after) {
        // 进行时间上的换算
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task);
        synchronized (this) {
            this.notify();
        }
    }
    // 这个 schedule 方法本身比较简单,只是单纯的把任务放到队列里去了
}

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);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3");
            }
        }, 3000);
    }
}

博主备注: 程序里的计时操作,本身就难以做到非常精确,因为操作系统调度线程有时间开销的.存在ms级别的误差,都很正常.也不影响日常使用.如果应用场景,就是对时间误差非常敏感(发射导弹,发射卫星)此时就不会再使用windows, linux这样的操作系统了,而应该使用像vxworks 这样的实时操作系统,这样的系统线程调度开销是极快,可控的,可以保证误差在要求范围内的.

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

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

相关文章

【立体视觉(五)】之立体匹配与SGM算法

【立体视觉&#xff08;五&#xff09;】之立体匹配与SGM算法 一、立体匹配一&#xff09;基本步骤二&#xff09;局部立体匹配三&#xff09;全局立体匹配四&#xff09;评价标准1. 均方误差(RMS)2. 错误匹配率百分比(PBM) 二、半全局(SGM)立体匹配一&#xff09;代价计算二&a…

雷达干扰和烧穿范围简介

一、干扰信号比 J/S或J-to-S是从目标发射的干扰信号接收的功率(J)与从目标的雷达反向散射接收的功率的比率。 二、烧穿范围 通过电子攻击(J)可以首先检测到目标回波信号(S)的雷达到目标的距离。 三、自保护干扰 也称为主瓣干扰(雷达回波源和干扰机并置)。 烧穿范围…

汽车驾驶任务的隐马尔可夫模型识别方法研究

汽车驾驶任务的隐马尔可夫模型识别方法研究 一、Introduction 自动驾驶汽车经过了几十年的发展&#xff0c;是目前国内外汽车行业中的重要研究方向。自 动驾驶汽车的智能化需要车辆能够有类“人”的行为&#xff0c;在决策策略上可以满足人的心理 需求。人在驾驶过程中&#…

Aasee Api开放平台上线啦!

使用方法 首先介绍使用方法&#xff0c;只需导入一个SDK即可使用实现调用第三方的接口&#xff0c;那如何导入SDK呢&#xff0c;目前jar已经上传至maven中心仓库可直接引入到pom文件中使用&#xff0c;下面是例子&#xff1a; <dependency><groupId>io.github.Aa…

攻防世界-T1 Training-WWW-Robots

文章目录 步骤1步骤二结束语 步骤1 看到文本——>提取有效信息——>利用有效信息 文本&#xff1a;In this little training challenge, you are going to learn about the Robots_exclusion_standard. The robots.txt file is used by web crawlers to check if they …

jar 命令启动java 指定配置文件路径 jar如何启动

一、各种启动方式 1.java -jar # 例子 java -jar test.jar 1. 2. 这是最简单的启动方式&#xff0c;同时弊端也是很多的。 弊端1&#xff1a;exit 退出终端会导致java进程中断。 弊端2&#xff1a;ctrlc 退出启动展示页会导致java进程中断。 弊端3&#xff1a;直接关闭终端会…

FREERTOS内容解惑与综合应用(基于STM32F103)

本文基础内容参考的是正点原子的FREERTOS课程。 这是基于HAL库的 正点原子手把手教你学FreeRTOS实时系统 这是基于标准库的 正点原子FreeRTOS手把手教学-基于STM32 基础知识&#xff0c;直接参考正点原子《FreeRTOS开发指南V1.1》基于标准库的&#xff0c;此处不再赘述。 本文…

【Java 进阶篇】HTML介绍与软件架构相关知识详解

HTML&#xff08;Hypertext Markup Language&#xff09;是一种用于创建网页的标记语言。它是互联网上信息传递和展示的基础&#xff0c;无论是在浏览器中查看网页还是在移动设备上浏览应用程序&#xff0c;HTML都扮演着关键角色。本文将向您介绍HTML的基础知识&#xff0c;并探…

踩大坑ssh免密登录详细讲解

目 录 问题背景 环境说明 免密登录流程说明 1.首先要在对应的用户主机名的情况下生成密钥对&#xff0c;在A服务器执行 2.将A服务器d公钥拷贝到B服务器对应的位置 3.在A服务器访问B服务器 免密登录流程 0.用户说明 1.目前现状演示 2.删除B服务器.ssh 文件夹下面的…

多普勒频率相关内容介绍

图1 多普勒效应 1、径向速度 径向速度是作用于雷达或远离雷达的速度的一部分。 图2 不同的速度 2、喷气发动机调制 JEM是涡轮机的压缩机叶片的旋转的多普勒频率。 3、多普勒困境 最大无模糊范围需要尽可能低的PRF&#xff1b; 最大无模糊速度需要尽可能高的PRF&#xff1b…

什么是TF-A项目的长期支持?

安全之安全(security)博客目录导读 问题&#xff1a;Trusted Firmware-A社区每六个月发布一次代码。然而&#xff0c;对于生产中的平台&#xff0c;该策略在维护、重要软件修复的向后兼容性、获得最新的安全缓解措施和整体产品生命周期管理方面不具备可扩展性。 开源软件项目&…

假期题目整合

1. 下载解压题目查看即可 典型的猪圈密码只需要照着输入字符解开即可得到答案 2. 冷门类型的密码题型&#xff0c;需要特意去找相应的解题思路&#xff0c;直接百度搜索天干地支解密即可 3. 一眼能出思路他已经给了篱笆墙的提示提示你是栅栏密码对应解密即可 4. 最简单的社会主…

【17】c++设计模式——>原型模式

原型模式的定义 c中的原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;其目的是通过复制&#xff08;克隆&#xff09;已有对象来创建新的对象&#xff0c;而不需要显示的使用构造函数创建对象&#xff0c;原型模式适用于创建复杂对象时&a…

Linux软硬链接和动静态库

本文已收录至《Linux知识与编程》专栏&#xff01; 作者&#xff1a;ARMCSKGT 演示环境&#xff1a;CentOS 7 软硬链接和动静态库 前言正文软硬链接原理使用 文件时间动静态库库介绍静态库静态库制作静态库的使用关于静态链接 动态库动态库制作动态库的使用关于动态链接 补充 最…

React18学习

17、React_JSX的注意事项 <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>JSX的注意</title><script src="./script/react.development.js"></script><script src=&…

秋招还没Offer怎么办?

作者 | 磊哥 来源 | Java中文社群 作者微信 | GG_Stone 如果你是双非院线、没有实习经历、没有出众的技术&#xff08;算法没刷一千道&#xff0c;也没做过 Spring Cloud 项目&#xff09;、现在还没有面试&#xff08;或只有少量的面试&#xff09;、并且目前还没有 Offer&…

1392. 最长快乐前缀

链接&#xff1a; 1392. 最长快乐前缀 题解&#xff1a; class Solution { public:string longestPrefix(string s) {if (s.size() < 0) {return "";}int MOD 1e9 7;// 构建26的n次方&#xff0c;预处理std::vector<long> pow26(s.size());pow26[0] 1…

频次直方图、KDE和密度图

Seaborn的主要思想是用高级命令为统计数据探索和统计模型拟合创建各种图形&#xff0c;下面将介绍一些Seaborn中的数据集和图形类型。 虽然所有这些图形都可以用Matplotlib命令实现&#xff08;其实Matplotlib就是Seaborn的底层&#xff09;&#xff0c;但是用 Seaborn API会更…

网页版”高德地图“如何设置默认城市?

问题&#xff1a; 每次打开网页版高德地图时默认定位的都是“北京”&#xff0c;想设置起始点为目前本人所在城市&#xff0c;烦恼的是高德地图默认的初始位置是北京。 解决&#xff1a; 目前网页版高德地图暂不支持设置起始点&#xff0c;打开默认都是北京&#xff0c;只能将…

Redisson—分布式服务

一、 分布式远程服务&#xff08;Remote Service&#xff09; 基于Redis的Java分布式远程服务&#xff0c;可以用来通过共享接口执行存在于另一个Redisson实例里的对象方法。换句话说就是通过Redis实现了Java的远程过程调用&#xff08;RPC&#xff09;。分布式远程服务基于可…