多线程案例-实现定时器

news2024/12/25 10:24:11

1.定时器是什么

定时器是软件开发中的一个重要组件,功能是当达到一个特定的时间后,就执行某个指定好的代码

定时器是一个非常常用的组件,特别是在网络编程中,当出现了"连接不上,卡了"的情况,就使用定时器做一些操作来止损

标准库中也提供了定时器

标准库中的Timer类

标准库提供了一个Timer类,Timer类的核心方法为schedule(安排,预定;将……列入计划表或清单)

schedule包含两个参数
第一个参数指定即将要执行的任务代码
第二个参数指定多多长时间之后执行(单位ms)

下面是用一下定时器

可以看到有两个参数

TimerTask类就是一个实现了Runnable接口的类,来描述指定的任务

delay是指定的时间后执行任务!

运行程序,经过指定的时间后,执行了run()中的语句

2.实现定时器

定时器的核心

注册任务后需要保证任务在指定的时间要被执行
单独在定时器内部,创建一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了就执行,没到就继续等待
一个定时器能连续注册N个任务,N个任务是按照最初约定的时间按顺序执行
这N个任务肯定需要一种数据结构来保存,不难发现,我们可以使用优先级队列,我们每个任务都是带有时间的,按照时间小的作为优先级高的,此时队首元素一定是最先要执行的任务,这时候扫描线程也只需要扫描队首元素即可,不必扫描整个队.如果队首元素没有到执行时间,那么其它元素也不可能到达执行时间!!

简而言之,定时器的核心:

1.有一个扫描线程,判断是否到执行时间.

2.还得有一个数据结构保存被注册的任务.

此处优先级队列是在多线程环境下使用的,因此要关注线程安全问题!自己手动加锁,或者使用标准库提供的PriorityBlockingQueue,它既有优先级又符合线程安全的要求

实现代码

我们先创建一个任务类

class MyTask{
    //任务内容
    private Runnable runnable;
    //任务指定的时间(ms时间戳表示)
    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();
    }

队列里的"任务"使用Runnable表示,描述的是任务的内容

使用时间戳描述任务什么时候被执行

然后创建一个定时器

class MyTimer{
    //扫描线程
    private Thread t = null;
    //阻塞优先级队列来保存任务
    private PriorityBlockingQueue<MyTask> queue =
            new PriorityBlockingQueue<>();
}

我们要给定时器类提供一个"schedule"方法来注册任务

//指定两个参数,一个是任务内容,一个是多长时间后执行任务
    public void schedule(Runnable runnable,long after){
        //注意时间的换算
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
    }

接下来要实现一个比较麻烦的操作,就是扫描线程的实现

public MyTimer(){
        t = new Thread(()->{
           while (true){
               try {
                   //取出队首元素,检查是否到执行时间了
                   //如果到了,就执行
                   //如果没到,就放回队列
                   //如果没有元素证明没有任务,会阻塞等待
                   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);
               }
           }
        });
    t.start();
    }

上述代码大致实现了扫描线程的功能,但是还存在两个问题

第一个问题,我们还要明确我们的任务优先级是怎样的,还没指定

此时我们如果测试:

public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }

因为两个任务的优先级关系还没用comparable来设置,或者单独实现一个比较器comparator

运行程序

第二个问题,如果队首任务是四点进行执行,在两点的时候,线程开始扫描,就会一直从队首取出检查,发现没到执行时间,又放回去,反反复复!!直到四点开始执行.我们使用优先级队列来存储的,放回元素,堆就会进行一次调整,将这个任务又调整至队首,下次取出,还是这个元素!

这个循环没有阻塞,会快速的进行循环,是没有意义的,占用了cpu资源.这种情况称为"忙等",我们要对代码进行调整,进行阻塞式等待,sleep,wait..

如果等待时间明确,我们使用sleep可行吗?

此处看似等待时间是明确的,但是我们可能任意时间会来一个新的任务调用schedule,注册任务,那么队首元素就换了,必须得扫描出来.

如果用sleep,那么在sleep过程中就可能注册新的任务,如果在队首元素执行的时间前就要执行新注册的任务,然而用的sleep,就会错过这个任务的执行了
因此使用wait()notify()更合适,使用wait()进行等待,如果有新任务调用schedule,就notify(),重新检查一次,计算等待的时间
并且,wait()还有个超时时间的版本,如果没有新任务,则最多等到队首元素的执行时间就自动唤醒了

这样改动之后,我们既不会一直重复无用操作,也不会错过执行新注册任务

线程安全问题

代码到这里,还有个线程安全问题

我们考虑一个极端情况

如果代码执行到wait之前,这个线程被调度走了,当线程又被调度执行时,接下来就要进行wait操作,它的wait时间是算好了的,比如curTime是13:00,getTime是14:00,即将会wait一个小时,但是还没执行wait.

在该线程被调度走的过程中,如果另一个线程调用了schedule,注册了一个13:30执行的任务,此时schedule会执行notify()将wait()唤醒,但是扫描线程的wait()还没有执行呢,所以notify并没有实际作用,虽然新任务插入到队列中了,也是在队首.但是这个线程紧接又执行wait()一个小时,错过了这次任务的执行时间13:30

这都是多线程随机调度产生的,take和wait操作并非是原子的,如果这个过程是原子的,给它加上锁,保证不会有新的任务过来,就解决问题了,换言之就是要保证每次notify时,确实都在wait!

我们将锁的粒度变大,保证take和wait操作是原子的,就不会出现线程安全问题了

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

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

相关文章

阿里妈妈Dolphin智能计算引擎基于Flink+Hologres实践

作者&#xff1a;徐闻春&#xff08;花名 陌奈&#xff09; 阿里妈妈事业部技术专家 本文整理至FlinkHologres实时数仓Workshop北京站&#xff0c;点击查看视频回放>>> 阿里妈妈数据引擎团队负责广告营销计算引擎Dophin的开发&#xff0c;目前支撑百万级广告主的营销…

ios打包证书申请流程

目前的APP开发&#xff0c;多端开发成为了主流&#xff0c;所以开发APP很多都是使用uniapp来开发&#xff0c;而且都是使用windows电脑来开发。但是在打包ios应用的时候&#xff0c;是需要一个p12格式的打包证书和profile描述文件的。 那么这两个文件如何申请呢&#xff1f;这…

关系抽取Casrel实现(Pytorch版)

前言 关系抽取是自然语言处理中的一个基本任务。关系抽取通常用三元组(subject, relation, object)表示。但在关系抽取中往往会面临的关系三元组重叠问题。《A Novel Cascade Binary Tagging Framework for Relational Triple Extraction》提出的CASREL模型可以有效的处理重叠关…

前端基础(十)_Dom自定义属性(带案例)

Dom自定义属性 1.1、为什么要用自定义属性 例&#xff1a;很多个 li 点击变颜色。 <ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8&…

LeetCode[373]查找和最小的K对数字

难度&#xff1a;中等题目&#xff1a;给定两个以 升序排列 的整数数组 nums1和 nums2, 以及一个整数 k。定义一对值 (u,v)&#xff0c;其中第一个元素来自 nums1&#xff0c;第二个元素来自 nums2。请找到和最小的 k个数对 (u1,v1), (u2,v2)... (uk,vk)。示例 1:输入: nums1 …

离散数学与组合数学-01集合论

文章目录1.离散数学与组合数学-01集合论1.1 集合定义1.1.1 什么是集合1.1.2 集合案例1.1.3 集合的符号表示1.2 集合表示1.2.1属于关系1.2.2 枚举法1.2.3 叙述法1.2.4 文氏图1.3 集合基数1.3.1 什么是集合基数1.3.2 集合基数案例1.4 集合间关系1.4.1 空集1.4.2 全集1.4.3 集合的…

【Vue + Koa 前后端分离项目实战9】使用开源框架==>快速搭建后台管理系统 -- part9 项目总结

人要保持忙碌&#xff0c;因为忙碌是世界上最便宜的药。 本博客教学视频来源于imoom 《0到1快速构建自己的后台管理系统》课程 官方演示地址&#xff1a;https://talelin.com/ 目录 一、项目介绍 1.技术准备 2.学到的内容 &#xff08;1&#xff09;后端接口实现的业务逻辑…

【Kotlin】标准库函数 ③ ( with 标准库函数 | also 标准库函数 )

文章目录一、with 标准库函数二、also 标准库函数Kotlin 语言中 , 在 Standard.kt 源码中 , 为所有类型定义了一批标准库函数 , 所有的 Kotlin 类型都可以调用这些函数 ; 一、with 标准库函数 with 函数 与 run 函数 功能是一样的 , 其使用形式不同 , with 函数是 独立使用的 …

LaoCat带你认识容器与镜像(三【下】)

说随缘就随缘&#xff0c;前天刚说惰怠今天就更新~。 本章内容 Docker端口映射相关。 本文实操全部基于Ubuntu 20.04 宿主机 > linux服务器本身 二章二小节中介绍docker create命令时就提供了常用的附加参数列表&#xff0c;docker run可附加参数同create命令附加参数一致&…

Flink【0】初识Flink快速又灵巧

文章目录一、Flink的引入二、Flink的起源和设计理念1.起源2.设计流程一、Flink的引入 随着大数据的飞速发展&#xff0c;出现了很多热门的开源社区&#xff0c;其中著名的有 Hadoop、Storm&#xff0c; 以及后来的Spark&#xff0c;他们都有着各自专注的应用场景。尤以Spark掀…

c++11 标准模板(STL)(std::forward_list)(四)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

软件测试:缺陷管理制度

缺陷管理制度 编制部门&#xff1a; 时间&#xff1a;编 制 人&#xff1a; 时间&#xff1a;标 准 化&#xff1a; 时间&#xff1a;审 核&#xff1a; 时间&#xff1a;批 准&#xff1a; …

(一)STM32L4(RT- Thread)——电机和蜂鸣器,独立按键,LED灯

&#xff08;一&#xff09;STM32L4&#xff08;RT- Thread&#xff09;——电机和蜂鸣器&#xff0c;独立按键&#xff0c;LED灯 文章目录&#xff08;一&#xff09;STM32L4&#xff08;RT- Thread&#xff09;——电机和蜂鸣器&#xff0c;独立按键&#xff0c;LED灯LED灯学…

5.8.2、TCP 的连接释放

1、释放流程图 释放流程图 2、TCP 使用 “四报文挥手” 释放连接的具体过程 TCP 通过 “四报文挥手” 来释放连接 数据传输结束后&#xff0c;TCP 通信双方都可以释放连接 现在 TCP 客户进程和 TCP 服务器进程都处于连接已建立状态\color{blue}连接已建立状态连接已建立状态…

k8s 文件 目录挂载

k8s 文件 目录挂载1.环境说明2.HostPath 挂载至宿主机3.挂载至nfs3.1 第一种3.2 第二种3.3 volumeClaimTemplates 挂载模板4.emptyDir-临时数据卷5.未完待续1.环境说明 k8s 1.24.3 2.HostPath 挂载至宿主机 apiVersion: v1 kind: Pod metadata:name: test-pd spec:container…

【Git 从入门到精通】Git是干什么的?

文章目录一、Git简介1.发展历史2.工作机制分析3.代码托管中心二、理解版本控制1.什么是版本控制&#xff1f;2.版本控制的好处3.版本控制的手段①集中式版本控制工具②分布式版本控制工具一、Git简介 Git 是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处…

Spool Print Fool(CVE-2022-21999 )提权漏洞

Spool Print Fool&#xff08;CVE-2022-21999 &#xff09;提权漏洞 文章目录 一、Print Spooler 简介二、漏洞简介三、影响版本四、本地复现五、漏洞修复 一、Print Spooler 简介 Print Spooler是打印后台处理服务&#xff0c;即管理所有本地和⽹络打印队列及控制所有打印⼯作…

Scala 模式匹配

文章目录Scala 模式匹配一、模式匹配1、基本语法2、语法说明3、案例示例二、模式守卫三、模式匹配类型1、匹配常量2、匹配类型3、匹配数组Scala 模式匹配 一、模式匹配 Scala 中的模式匹配类似于Java中的switch语法&#xff0c;但是scala 从语法中补充了更多的功能&#xff0…

Optional更优雅的判空

我们在日常的开发工作最经常碰到的异常就是空指针异常&#xff0c;排查起来非常麻烦影响开发效率。如果想避免这种问题&#xff0c;就要写大量的if(xx ! null){}这样的冗余代码&#xff0c;把自己的逻辑写入到括号内部&#xff0c;这样的代码看起来臃肿繁琐。JDK8中为我们提供了…

IP 分支组网解决方案

IP 分支机构组网解决方案在总部部署高性能设备作为中心网关&#xff0c;在各地分支机构分别部署一台产品作为分支机构接入网关&#xff0c;与总部网关进行加密的连接。融合多种产品特性&#xff0c;可有效、快速、安全的实现分支机构的远程接入。同时&#xff0c;采用集中管控平…