【多线程】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.rhkb.cn/news/127198.html

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

相关文章

2023国赛C题解题思路代码及图表:蔬菜类商品的自动定价与补货决策

2023国赛C题&#xff1a;蔬菜类商品的自动定价与补货决策 C题表面上看上去似乎很简单&#xff0c;实际上23题非常的难&#xff0c;编程难度非常的大&#xff0c;第二题它是一个典型的动态规划加仿真题目&#xff0c;我们首先要计算出销量与销售价格&#xff0c;批发价格之间的…

Text-to-SQL小白入门(四)指令进化大模型WizardLM

摘要 本文主要对大模型WizardLM的基本信息进行了简单介绍&#xff0c;展示了WizardLM取得的优秀性能&#xff0c;分析了论文的核心——指令进化方法。 论文概述 基本信息 英文标题&#xff1a;WizardLM: Empowering Large Language Models to Follow Complex Instructions中…

Linux下 /sys/class 一些操作

Linux下&#xff0c;/dev、/sys/class的区别 /dev下面有很多节点&#xff0c;每一个节点代表一个设备&#xff0c;/dev目录下面是按物理器件进行分类&#xff1b;而/sys/class下面的更多是按功能抽象出来的。 参考1 demo 在正点原子的基础上进行演示 #include <linux/ty…

【Spring面试】BeanFactory与IoC容器的加载

文章目录 Q1、BeanFactory的作用是什么&#xff1f;Q2、BeanDefinition的作用是什么&#xff1f;Q3、BeanFactory和ApplicationContext有什么区别&#xff1f;Q4、BeanFactory和FactoryBean有什么区别&#xff1f;Q5、说下Spring IoC容器的加载过程&#xff08;※&#xff09;Q…

it运维监控管理平台,统一运维监控管理平台

随着系统规模的不断扩大和复杂性的提高&#xff0c;IT运维管理的难度也在逐步增加。为了应对这一挑战&#xff0c;IT运维监控管理平台应运而生。本文将详细介绍IT运维监控管理平台的作用和优势以及如何选择合适的平台。 IT运维监控管理平台的作用管理平台 IT运维监控管理平台是…

母婴品牌的小红书投放策略有哪些,投放总结

最近有很多人想知道达人投放的一些相关知识&#xff0c;我们立马捕捉到了大家对这一干货内容的感兴趣程度&#xff0c;今天就来为大家分享下&#xff0c;母婴品牌的小红书投放策略有哪些&#xff0c;投放总结&#xff01; 什么是达人投放? 达人投放是一种常用于社交媒体营销的…

【动态规划刷题 12】等差数列划分 最长湍流子数组

139. 单词拆分 链接: 139. 单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 示例 1&#xff1a; 输入: …

Browserslist 信息和配置使用整理

我们可以在各种前端工程看到 Browserslist 的配置身影&#xff0c;看似简单但实际上可能会有暗坑导致线上兼容问题&#xff0c;借此文来整理下 Browserslist 的信息。 Browserslist 是由 Autoprefixer 团队维护的一个开源项目&#xff0c;用于自动处理 CSS 和 JavaScript 文件…

Open3D(C++) 整体最小二乘拟合平面

目录 一、算法原理1、算法过程2、参考文献二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。 一、算法原理 1、算法过程 最小二乘拟合平面认为点云数据系数矩阵不存在误差,然而由于观测条件的限制,观测向量、系数矩阵都有可能存在误差,那么最小二乘方法就不再是最…

Vue中的图标

Vue中的图标 https://iconpark.oceanengine.com/official 官方教程&#xff1a;icon-park/vue - npm 1.IconPark 2.基本使用 下载 yarn add icon-park/vue --save 启动 yarn run serve 项目中引用 <script> import { TableFile } from icon-park/vue; export defa…

对卷积的一点具象化理解

前言 卷积的公式一般被表示为下式&#xff1a; 对新手来说完全看不懂这是干什么&#xff0c;这个问题需要结合卷积的应用场景来说。 原理 卷积比较广泛的应用是在信号与系统中&#xff0c;所以有些公式的定义会按照信息流的习惯。假设存在一串信号g(x)经过一个响应h(x)时他的响…

Linux内核分析与应用

Linux 内核分析与应用[1] 蜻蜓点水,可作抛砖引玉 1.概述 用到的几个命令: insmod dmesg[2] lsmod[3] 章节测试: 部分可参考[4] <1>. Linux得以流行&#xff0c;是因为遵循了GPL协议&#xff0c;并不是因为遵循POSIX标准 (错) linux操作系统概述[5] linux概述[6] <2>…

Mysql中in和exists的区别 not in、not exists、left join的相互转换

文章目录 1. in 介绍1.1 in中数据量的限制1.2 null值不参与in或not in&#xff0c;也就是说in and not in 并不是全量值&#xff0c;排除了null值1.3 in的执行逻辑 2. exists介绍2.1 exists not exists 是全量数据2.2 exists的执行逻辑 3. 小表驱动大表的好处4. in、not in、e…

开源对象存储系统minio部署配置与SpringBoot客户端整合访问

文章目录 1、MinIO安装部署1.1 下载 2、管理工具2.1、图形管理工具2.2、命令管理工具2.3、Java SDK管理工具 3、MinIO Server配置参数3.1、启动参数&#xff1a;3.2、环境变量3.3、Root验证参数 4、MinIO Client可用命令 官方介绍&#xff1a; MinIO 提供高性能、与S3 兼容的对…

30天入门Python(基础篇)——第1天:为什么选择Python

文章目录 专栏导读作者有话说为什么学习Python原因1(总体得说)原因2(就业说) Python的由来(来自百度百科)Python的版本 专栏导读 &#x1f525;&#x1f525;本文已收录于《30天学习Python从入门到精通》 &#x1f251;&#x1f251;本专栏专门针对于零基础和需要重新复习巩固…

恒运资本:开盘时间是几点到几点?

开盘时刻是指各种商场的正式开端生意时刻&#xff0c;包括股票商场、外汇商场、商品期货商场等。关于出资者来说&#xff0c;了解开盘时刻是十分重要的&#xff0c;由于它直接关系到生意的时刻和机会。本文将从多个角度分析开盘时刻的重要性、不同商场的开盘时刻以及对出资者带…

tf和pytorch每轮epoch显示输出的auc是如何计算的

tf和pytorch每轮epoch显示输出的auc是如何计算的&#xff1f; tf的计算 近似 ROC 或 PR 曲线的 AUC&#xff08;曲线下面积&#xff09;。 tf1 通过计算真阳性&#xff0c;假阳性&#xff0c;假阴性&#xff0c;真阴性值的计算策略。 tensorflow AUC & streaming_auc_我…

私有gitlab的搭建和配置教程

文章目录 1. 说明2. 安装操作2.1 依赖项2.2 gitlab-ce2.3 简要配置2.4 网页操作2.5 中文配置2.6 其他操作 3. 配置https3.1 配置证书3.2 url配置3.3 网页访问3.4 认证错误 4. ssh操作4.1 生成文件4.2 web配置4.3 额外操作 1. 说明 此教程基于Ubuntu22.04进行阐述&#xff0c;只…

【iOS】属性关键字

文章目录 前言一、深拷贝与浅拷贝1、OC的拷贝方式有哪些2. OC对象实现的copy和mutableCopy分别为浅拷贝还是深拷贝&#xff1f;3. 自定义对象实现的copy和mutableCopy分别为浅拷贝还是深拷贝&#xff1f;4. 判断当前的深拷贝的类型&#xff1f;(区别是单层深拷贝还是完全深拷贝…

基于SSM的农产品推广应用网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…