【多线程】Timer任务定时器实现与盲等原子性问题的解决

目录

一、定时器

二、标准库中的Timer

三、代码实现

四、死锁


一、定时器

代码中的定时器通常是在一定的时间执行对应的代码逻辑

二、标准库中的Timer

public static void main(String[] args){Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("时间到了后执行业务逻辑");}},1000);}

一个timer可以执行多个定时任务,后续添加任务继续调用schedule方法即可

三、代码实现

首先我们定义一个类用于描述任务

// 用于描述任务
class MyTimerTask {// 执行的任务private Runnable runnable;// 什么时间后执行(绝对的时间)private long time;/**** @param runnable 任务* @param time 多少时间后执行*/public MyTimerTask(Runnable runnable, long time) {this.runnable = runnable;this.time = System.currentTimeMillis() + time;}
}

然后我们开始定时器的编写,首先我们需要一个数据结构来存储提交的定时任务,这个数据结构需要能够依次取出最先执行的任务且要是线程安全的,首先想到的是优先级队列其次要有阻塞功能就是阻塞队列,然后我们需要定义一个提交任务的方法该方法中可以将提交的任务存入该队列中,然后在构造方法中创建一个扫描线程不断地取出该队列里地任务进行执行。在此之前我们使用优先级队列是存储定时任务的,那么我们可以先给上面的类实现Compareable接口重写compareTo方法

class MyTimer {// 1. 创建存储定时任务的数据结构BlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();// 2. 定义提交定时任务的方法public void schedule(Runnable runnable,long after) throws InterruptedException {MyTimerTask task = new MyTimerTask(runnable,after);queue.put(task);}// 3. 构造方法中定义扫描线程public MyTimer() {new Thread(()->{// 3.1 不断的取出数据看是否需要执行while (true) {try {// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间MyTimerTask task = queue.take();if (System.currentTimeMillis() >= task.getTime()) {// 3.1.2 到达时间执行任务task.getRunnable().run();} else {// 3.1.3 没到时间重回队列queue.put(task);}} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}

这个时候盲等问题就出现了,比如我们提交了一个2小时后才执行的任务,但是按照上述代码则在这2小时的时间里不断地从队列中取出该任务比较后重回队列,那可以使用sleep(2h)这种方法来解决问题吗?答案是不能,使用sleep方法让线程挂起两个小时可以保证2小时后的任务会被执行,但是如果中途有其他更早的任务提交进来,那么这个任务就会错过执行的时间。那让每次sleep的时间短一点呢?答案同理也是不能的。我们可以使用wait方法来实现,wait(2h)然后在提交任务的方法中一但有新的任务提交调用notify唤醒wait即可,如果在这两个小时内没有任务提交,那么该方法还是会在2h后去执行任务。

 

// 2. 定义提交定时任务的方法public void schedule(Runnable runnable,long after) throws InterruptedException {MyTimerTask task = new MyTimerTask(runnable,after);queue.put(task);synchronized (this) {this.notify();}}// 3. 构造方法中定义扫描线程public MyTimer() {new Thread(()->{// 3.1 不断的取出数据看是否需要执行while (true) {try {// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间MyTimerTask task = queue.take();if (System.currentTimeMillis() >= task.getTime()) {// 3.1.2 到达时间执行任务task.getRunnable().run();} else {// 3.1.3 没到时间重回队列queue.put(task);// 3.1.4 阻塞synchronized (this) {this.wait(task.getTime() - System.currentTimeMillis());}}} catch (InterruptedException e) {e.printStackTrace();}}}).start();}

这样我们就解决了盲等问题,但是还有一个原子性问题,就是如果此时扫描线程刚好取出了最先要执行的任务该任务是在2h后执行,扫描线程在判断是否到达执行时间之前,其他线程调用添加任务的方法加入了一个1h后需要执行的任务且方法执行完notify没有起到任何作用,此时扫描线程判断完后发现还没有到时间于是将任务入队后调用wait方法等待2h或被唤醒,刚好这2h没有其他任务加入,那么之前提交的1h后的任务就会延迟执行。这是由于扫描线程中操作不是原子性的我们需要调整锁的粒度

new Thread(()->{// 3.1 不断的取出数据看是否需要执行while (true) {try {synchronized (this) {// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间MyTimerTask task = queue.take();if (System.currentTimeMillis() >= task.getTime()) {// 3.1.2 到达时间执行任务task.getRunnable().run();} else {// 3.1.3 没到时间重回队列queue.put(task);// 3.1.4 阻塞this.wait(task.getTime() - System.currentTimeMillis());}}} catch (InterruptedException e) {e.printStackTrace();}}}).start()

 那么notify代码中的锁粒度是否也需要调整呢?

四、死锁

如果我们将schedule方法中锁的粒度也扩大

public void schedule(Runnable runnable,long after) throws InterruptedException {synchronized (this) {MyTimerTask task = new MyTimerTask(runnable,after);queue.put(task);this.notify();}}

这个时候我们进行测试会发现什么也不会执行,发送了死锁。那这是为什么呢?

首先MyTimer实例被创建时扫描线程开始执行当他执行到此处时会因为阻塞队列中还没有元素而阻塞等待

但是锁还是被持有,此时提交任务的代码执行时发现需要先获取到锁,但是锁是被扫描线程持有,于是他需要阻塞等待,但是扫描线程中的take方法也需要执行了提交任务方法中的put才能继续执行,但是执行put方法有需要扫描线程先释放锁,所以发生死锁,这个时候我们需要将schedule方法中锁的粒度修改回去

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue+NodeJS上传图片到腾讯云Cos

一.前端Vue 1.选择图片 --HTML <input type"file" accept"image/*" change"handleFileChange"> <el-button size"large" click"changeAvatar">上传头像</el-button> //选择图片 function handleFileC…

SpringAOP的实现机制(底层原理)、应用场景等详解

SpringAOP的实现机制&#xff08;底层原理&#xff09;应用场景等详解 ​ Spring框架是Java开发中最流行的应用程序框架之一。它提供了广泛的功能&#xff0c;其中之一就是面向切面编程&#xff08;AOP&#xff09;。Spring AOP允许我们将关注点&#xff08;例如日志记录、事务…

js摄像头动态检测

利用摄像头每一秒截图一次图像。然后计算2次图像之间的相似度。 如果相似度低于98%就会报警。 var video document.getElementsByClassName(inputvideo)[0]; video.innerHTML "<video classinput_video idcamera autoplay width640px height380px></video>…

利用procrank和lsof定位出客户软件内存OOM的问题

最近遇到一些事情&#xff0c;觉得挺憋屈的&#xff0c;可是再憋屈总得往前走吧&#xff01;打工人&#xff0c;不好办啊&#xff01;事情是这样的&#xff0c;笔者在芯片原厂负责SDK和行业解决方案输出的&#xff0c;可以理解成整体SDK turnkey方案。但是有些客户多少还要改一…

CSS笔记(黑马程序员pink老师前端)圆角边框

圆角边框 border-radius:length; 效果显示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Documen…

【Linux】网络编程网络基础(C++)

目录 一、计算机网络背景 二、认识 "协议" 三、网络协议初识 【3.1】协议分层 【3.2】OSI七层模型 【3.3】TCP/IP五层(或四层)模型 四、网络传输基本流程 【4.1】网络传输流程图 【4.2】数据包封装和分用 五、网络中的地址管理 一、计算机网络背景 【独立…

【周末闲谈】如何利用AIGC为我们创造有利价值?

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️周末闲谈】 系列目录 ✨第一周 二进制VS三进制 ✨第二周 文心一言&#xff0c;模仿还是超越&#xff1f; ✨第二周 畅想AR 文章目录 系列目录前言AIGCAI写作AI绘画AI视频生成AI语音合成 前言 在此之…

用Navicat备份Mysql演示系统数据库的时候出:Too Many Connections

今天用Navicat进行数据备份的时候&#xff0c;发现由于数据库连接数目过多导致连接锁定&#xff0c;这种情况在多人协同开发的场景中很常见。当然我这里也因为多个应用使用了数据库连接&#xff0c;所以出现了Too Many Connections。 可能是超过最大连接数了。 1、进入Navicat…

AUTOSARCAN-Tp协议

目录 一.单帧、首帧、连续帧、流控帧 单帧传输 SF单帧&#xff1a; 多帧传输 FF&#xff08;首帧&#xff09;&#xff1a; CF&#xff08;连续帧&#xff09;&#xff1a; FC&#xff08;流控帧&#xff09;&#xff1a; 一.单帧、首帧、连续帧、流控帧 CAN诊断由发送端…

MAC ITEM 解决cd: string not in pwd的问题

今天使用cd 粘贴复制的路径的时候,报了这么一个错. cd: string not in pwd eistert192 Library % cd Application Support cd: string not in pwd: Application eistert192 Library % 让人一脸懵逼. 对比一下,发现中文路径里的空格截断了路径 导致后面的路径就没有办法被包含…

视频汇聚/视频云存储/视频监控管理平台EasyCVR启动时打印starting server:listen tcp,该如何解决?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;可实现视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、H.265自动转码H.264、平台级联等。为了便于用户二次开发、调用与集成&#xff0c;…

模糊测试面面观 | 模糊测试是如何发现异常情况的?

协议模糊测试是一种用于评估通信协议、文件格式和API实现系统安全性和稳定性的关键技术。在模糊测试过程中&#xff0c;监视器扮演着关键角色&#xff0c;它们能够捕获异常情况、错误响应、资源利用等&#xff0c;为测试人员提供有价值的信息&#xff0c;有助于发现潜在漏洞和问…