【多线程案例】定时器

news2025/1/14 18:37:04

1. 定时器是什么?

定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件. 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.

2. 使用标准库中的定时器

  • 标准库中提供了一个 Timer 类(java.util包下面). Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).
public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 3000);
    }

其中TimerTask()就是一个实现了Runnable的抽象类:

可以把它的作用看成是给定时器一个任务,而第二个参数就是指定多久时间后执行这个任务。

3. 手写代码实现定时器

思考一下定时器的构成需要哪些?

  • 一个带有优先级的阻塞式队列
  • 队列中的每一个元素都是一个“任务”对象
  • “任务”对象中包含两个属性,一个属性用于描述任务,也就是一个Runnable,另一个属性用来定义delay。如此一来对手元素就是最即将要执行的任务。
  • 同时需要有一个线程不停的扫描队首元素。看队首元素是否到了执行时间。

1)写一个任务类,任务类还必须能够按照时间来比大小,因为优先级阻塞队列需要比较大小

//任务类 描述任务和任务的delay时间
    static class Task implements Comparable<Task>{
        //任务
        private Runnable command;
        //delay
        private long time;

        public Task(Runnable command,long time){
            this.command = command;
            //时间是在现在的时间的基础上加上delay
            this.time = System.currentTimeMillis() + time;
        }

        public void run(){
            command.run();
        }

        @Override
        public int compareTo(Task o) {
            return (int)(this.time - o.time);
        }
    }

 2)需要有一个优先级阻塞队列来存放用户注册的任务

//优先级阻塞队列 核心结构
    //队首存放的是最近要执行的任务 time最小
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();

    public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);
    }

3)在构造方法中整一个线程对队首元素扫描,看是否到了执行时间

public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

用写一个测试:

    //测试代码
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第一个任务!");
            }
        },3000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第二个任务!");
            }
        },1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第三个任务!");
            }
        },2000);
    }

此时已经可以按照定时器的工作原理来完成任务了:

 但是当前的代码还存在着比较严重的问题,就是在3)中如果时间没有到的话会存在cpu一直比较的情况。举个例子,比如小明九点上班,他七点在床上突然醒了。正常情况下应该是继续睡睡到平时订的闹钟时间,但是如果小明一直看表一直看表知道闹铃响起,这样既没有休息也没有做有意义的事情,是十分愚蠢的行为。代码的问题也就在于此,如果没有到执行时间,不管还有多久还都会一直比较有没有到执行时间是没有意义的,也就是处于”忙等“状态。

优化的话,应该让系统在看到当前队首任务还没有到达执行时间的时候就执行wait(时间差)。但是此时还存在另外一个问题,系统wait一段时候之后确实会执行队首的任务,但是如果在wait的时间中又来了新的任务并且新的任务重新处于了队首,此时就会出bug了。正确的做法是在每次有新的任务被注册的时候都通知一下结束wait。

修改代码:

1.引入一个lock对象,借助该对象的wait/notify来解决忙等状态

private Object lock = new Object();

2.修改构造方法中的work的工作方法

public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        // 等待一段时间
                        synchronized (lock){
                            lock.wait(task.time - curTime);
                        }
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

3. 修改 Timer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 worker 线程. (因为新插入的任务可能是需要马上执行的).

public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);

        //有新任务来了 唤醒work 检测是否有更新的工作需要执行
        synchronized (lock){
            lock.notify();
        }
    }

完整代码:

public class MyTimer {
    //任务类 描述任务和任务的delay时间
    static class Task implements Comparable<Task>{
        //任务
        private Runnable command;
        //delay
        private long time;

        public Task(Runnable command,long time){
            this.command = command;
            //时间是在现在的时间的基础上加上delay
            this.time = System.currentTimeMillis() + time;
        }

        public void run(){
            command.run();
        }

        @Override
        public int compareTo(Task o) {
            return (int)(this.time - o.time);
        }
    }

    //优先级阻塞队列 核心结构
    //队首存放的是最近要执行的任务 time最小
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();

    public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);

        //有新任务来了 唤醒work 检测是否有更新的工作需要执行
        synchronized (lock){
            lock.notify();
        }
    }

    private Object lock = new Object();

    public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        // 等待一段时间
                        synchronized (lock){
                            lock.wait(task.time - curTime);
                        }
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

    //测试代码
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第一个任务!");
            }
        },3000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第二个任务!");
            }
        },1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第三个任务!");
            }
        },2000);
    }
}

此时代码还有问题吗????

理论上说说代码中还是有一点小问题的。(烧脑啊.....)上图:

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

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

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

相关文章

【Markdown】图片缩放

▚ 01 原图表示 语法为&#xff1a; ![替代文本](图片链接地址)其中&#xff0c;替代文本是在无法显示图片时显示的替代文本&#xff0c;而图片链接是指向图片的URL或相对路径。 例如&#xff0c;插入Panda图片&#xff1a; ![panda](https://img-blog.csdnimg.cn/e5f3…

李开复:我家的AI是坠吼的

创新工场董事长、鸿海独立董事李开复&#xff0c;近日出席鸿海股东会暨媒体记者会时表示&#xff0c;人工智能&#xff08;AI&#xff09;是人类史上即将面临的最伟大技术革命&#xff0c;未来十年的改变将改写人类历史、重构所有产业&#xff0c;其发展大致可分三阶段&#xf…

uniapp风险等级(三级)

代码 ​ <template><view><view class"riskGrade"><label>风险等级: </label><span v-if"flag 0 || flag 1 || 2" class"item":style"[{background:flag0?color:flag1?color:flag2?color:}]"…

快速排序和归并排序的非递归形式

快速排序和归并排序都需要用递归的形式展开&#xff0c;那么有没有什么方法不需要递归就能实现归并和快速排序&#xff0c;有的&#xff01; 1.快速排序 我们可以借助栈来模拟递归。 递归的主要思想就是大事化小&#xff0c;小事化了。我们借助栈的 目的是将需要排序的“头” 和…

面试题:有了 for 循环 为什么还要 forEach ?

文章目录 **本质区别****for循环和forEach的语法区别****for循环和forEach的性能区别** js中那么多循环&#xff0c;for for…in for…of forEach&#xff0c;有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异。我们从几个维度展开讨论&#xff1a; for循环和fo…

RJ45网络信号浪涌保护器解决方案

RJ45网络信号浪涌保护器是一种用于保护网络设备免受雷击或其他高压电流干扰的装置&#xff0c;它可以有效地吸收和释放信号线路上的过电压&#xff0c;从而避免设备损坏或数据丢失。 RJ45信号浪涌保护器的应用领域和施工方案如下&#xff1a; 地凯科技RJ45网络信号浪涌保护器…

合肥综合性国家科学中心人工智能研究院-机器学习作业(一)

1.试析min-max规范化和z-score规范化的优缺点 可参考博客&#xff1a;https://wenku.csdn.net/answer/fdbf30eb204644e5b69fc533a3757268 2.试分析损失函数与性能度量的关系 损失函数和性能度量之间的关系可以根据优化目标来理解。损失函数的优化目标是最小化预测值与实际值之…

laravel框架 - 开发实战(目录结构,路由,控制器,模型,视图)

一、laravel框架的目录结构 app:应用目录&#xff0c;保存项目中的控制器、模型等 bootstrap:保存框架启动的相关文件 config:配置文件目录 database:数据库迁移文件和数据填充文件 public:应用入口文件index.php和前端资源文件&#xff08;如CSS、JavaScript等&#xff09…

都2023年了你还不学ts (一)

TypeScript基础语法入门 TypeScript究竟是什么&#xff1f; 他主要就是想把JavaScript里面不完美的一些语法来进行一个提升 就像官网中所说的 TypeScript is JavaScript with syntax for types. 例如我们看下面的这段代码 if ("" 0) {console.log(hello) }在Jav…

VS新功能:智能添加函数标记

今天&#xff0c;我们官宣 Visual Studio 预览版的两项新功能&#xff1a; 1) 标记类成员函数为 const。 2) 标记全局函数为 static。 标记类成员函数为 const 如果一个类成员函数不会修改对象的状态&#xff0c;则 Visual Studio 会给出建议&#xff0c;将这个函数标记为 c…

Python统计pdf中英文单词的个数

之前的文章提供了批量识别pdf中英文的方法,详见【python爬虫】批量识别pdf中的英文,自动翻译成中文上。以及自动pdf英文转中文文档,详见【python爬虫】批量识别pdf中的英文,自动翻译成中文下。    本文实现python统计pdf中英文字符的个数。 文章目录 一、要统计字符的pdf…

JDK14特性——GC的改进和优化

文章目录 G1的NUMA内存分配优化NUMA介绍目标 弃用SerialCMS,ParNewSerial Old理由 删除CMSCMS弊端其他垃圾收集器 ZGC on macOS and Windows G1的NUMA内存分配优化 NUMA介绍 NUMA就是非统一内存访问架构&#xff08;英语&#xff1a;non-uniform memory access&#xff0c;简…

【Flask】会话保持-API授权-注册登录

http - 无状态-无法记录是否已经登陆过 #会话保持 – session cookie session – 保存一些在服务端 cookie – 保存一些数据在客户端 session在单独服务器D上保存&#xff0c;前面数个服务器A,B,C上去取就好了&#xff0c;业务解耦。—》》现在都是基于token的验证。 以上是基…

SpringMVC自定义注解和使用

一.引言 1.简介&#xff1a; 在Spring MVC中&#xff0c;我们可以使用自定义注解来扩展和定制化我们的应用程序。自定义注解是一种通过Java的注解机制定义的特殊注解&#xff0c;可以应用于控制器类、方法或者方法参数上&#xff0c;以实现不同的功能和行为。&#xff08;注解…

[uni-app] iOS/Android端 禁止单个页面侧滑返回的处理记录

需要禁止部分页面侧滑返回 iOS端 popGesture 但是实测后, 其实设置popGesture:none 是无效的 真正可以用的是这个 disableSwipeBack android端 别的方案没有逐一尝试, 这边可以在需要禁止的页面的 onBackPress onBackPress() {// 禁止侧滑(但注意也会禁止导航返回,导航返…

如何在外网访问公司项目?快解析实现内网ip让公网连接

随着互联网技术的不断发展&#xff0c;越来越多的企业和个人选择使用服务器进行网站或应用程序的部署。公司内部项目需要提供外网访问是个常见的网络场景&#xff0c;需要怎么操作设置也是网络或项目人员需要关注的。 企业使用服务器搭建公司业务系统一般会使用云服务器或者使…

最新AI创作系统ChatGPT源码/支持国内AI模型/支持GPT4.0/支持AI绘画

一、AI创作系统 SparkAi系统是基于国外很火的ChatGPT进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT系统&#xff1f;小编这里写一个详细图文教程吧&am…

WebGPU学习(10)---如何利用 WebGPU 实现高性能

虽然是WebGPU&#xff0c;但是速度很慢&#xff01;&#xff1f; 我们将解释如何充分利用 WebGPU 性能。这次我们以绘制大量物体为例&#xff0c;根据“使用纹理”中的代码进行一些更改并绘制 900 个立方体。 要均匀分布立方体&#xff0c;可以按如下方式更新 worldMatrix&am…

微信小程序 解决 当套在scroll-view中后 wx.pageScrollTo 函数失效问题解决

pageScrollTo 只是 页面的API 他对 scroll-view 的滚动是无法控制的 但是 scroll-view 也提供了一个scroll-into-view属性 我们编写一个小案例 wxml 参考代码如下 <view><scroll-view scroll-y"{{ true }}" style"height: 100vh;" scroll-into-v…

笑笑云航服悦《乡村振兴战略下传统村落文化旅游设计》许少辉博士新著

笑笑云航服悦《乡村振兴战略下传统村落文化旅游设计》许少辉博士新著