定时器: Timer (Java)

news2024/11/21 1:39:17

定时器: Timer

  • 一、定时器是什么
  • 二、标准库中的定时器
  • 三、实现定时器
    • 3.1 定时器的构成
    • 3.2 实现细节
    • 3.3 完整代码

一、定时器是什么

定时器也是软件开发中的一个重要组件。类似于一个 “闹钟”,即达到一个设定的时间之后,就执行某个指定好的代码。
在这里插入图片描述

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

二、标准库中的定时器

  • 标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule。
  • schedule 包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行 (单位为毫秒)。
import java.util.Timer;
import java.util.TimerTask;

public class Demo {
    public static void main(String[] args) {
        // 标准库的定时器.
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到, 快起床!");
            }
        }, 3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到2!");
            }
        }, 4000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到3!");
            }
        }, 5000);

        System.out.println("开始计时!");
    }
}

执行完上述任务之后,进程并没有退出!
因为Timer内部需要一组线程来执行注册的任务,而这里的线程是前台线程,会影响进程退出~~

三、实现定时器

3.1 定时器的构成

  • 一个带优先级的阻塞队列;

为啥要带优先级呢?
因为阻塞队列中的任务都有各自的执行时刻 (delay),最先执行的任务一定是 delay 最小的,使用带优先级的队列就可以高效地把这个 delay 最小的任务找出来~~

  • 队列中的每个元素是一个 Task 对象;
  • Task 中带有一个时间属性,队首元素就是时间最小的 Task;
  • 同时有一个 worker 线程一直扫描队首元素,看队首元素是否需要执行!

3.2 实现细节

1)schedule第一个参数是一个任务,包含两个信息:一个是要执行啥工作;一个是啥时候执行

// 这个类表示一个任务class MyTask
// 要执行的任务
private Runnable runnable;
// 什么时间来执行任务(是一个时间戳) 
private long time;

public MyTask(Runnable runnable,long delay) {
	this.runnable = runnable;
	this.time = System.currentTimeMiLlis() + delay;
}

2)让 MyTimer 能够管理多个任务 (一个Timer是可以安排多个任务的)

前面提及:一个带优先级的阻塞队列最合适!

private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

3)任务已经被安排到优先级阻塞队列中了,接下来就需要从队列中取元素了。
创建一个单独的扫描线程,让这个线程不停的来检查队首元素,看时间是否到了。如果时间到了,则执行该任务!
在这里插入图片描述
4)优先级阻塞队列需要进行元素比较,所以要实现Comparable接口!

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

5)还存在严重的问题!!!
在这里插入图片描述
可以使用wait来阻塞线程,当schedule加入新任务时再唤醒!:

Thread t = new Thread(() -> {
	while (true){
		try {
			//取出队首元素
			MyTask task = queue.take();
			//假设当前时间是2:30,任务设定的时间是2:30,显然就要执行任务了
			//假设当前时间是2:30,任务设定的时间是2:29,也是到点了,也要执行任务long curTime = System.currentTimeMillis();
			if (curTime >= task.getTime()){
				//到点了,改执行任务了!!
				task.getRunnable(). run();
			} else {   
				//还没到点
				queue.put(task);
				//没到点,就等待
				synchronized (locker){
					locker.wait( timeout: task.getTime() - curTime);
				}
		    }
		}catch (InterruptedException e) {
				e.printstackTrace();
			}
		}
	});

public void schedule(Runnable runnable, long after) throws InterruptedException {
    MyTask myTask = new MyTask(runnable, after);
    queue.put(myTask);
    synchronized (locker) {
        locker.notify();
    }
}

这时的代码还有一个问题!!!
假设扫描线程先执行,执行take之后线程切换到schedule线程。
schedule线程新增一个任务,这个任务1:00 执行。schedule执行完毕之后,执行notify ( t线程刚执行完take,还没wait呢~ 这个notify相当于空打了一炮:虽然通知了,但是没有唤醒任何线程),然后回到扫描线程继续往下执行,然后发现当前时刻是12:00,任务时间是2:30?!这时就把任务塞回队列,然后就进行wait,wait时间是2.5小时!
这就意味着,刚才新来的这个1:00要执行的任务,就被错过了!!!

多线程的执行过程是非常复杂的,任何两行代码之间都可能出现线程切换,甚至一行代码中就可能会切换多次~~
写代码的时候脑子里就得演绎出各种各样的情况!!!

因此需要把锁的范围放大

public MyTimer() {
    // 创建一个扫描线程.
    Thread t = new Thread(() -> {
        while (true) {
            try {
                synchronized (locker) {
                    // 取出队首元素
                    MyTask task = queue.take();
                    // 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
                    // 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
                    long curTime = System.currentTimeMillis();
                    if (curTime >= task.getTime()) {
                        // 到点了, 改执行任务了!!
                        task.getRunnable().run();
                    } else {
                        // 还没到点
                        queue.put(task);
                        // 没到点, 就等待
                        locker.wait(task.getTime() - curTime);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

刚才出现问题的原因就是notify在take和wait之间执行的。
现在把扫描线程中的锁范围放大了,此时就可以避免notify在take和wait之间执行了!扫描线程会先拿到锁,然后take,然后中间逻辑,一直到wait;在这个过程中,schedule线程会阻塞等待锁。直到扫描线程执行了wait后,扫描线程释放了锁,schedule线程就拿到了锁,进行了通知,这个时候wait就被立即唤醒了!接下来再次重新取队首元素,就把1:00执行的任务取出来了~~

如果把schedule方法里的锁范围也扩大可以吗?:

public void schedule(Runnable runnable, long after) throws InterruptedException {
   	synchronized (locker) {
    	MyTask myTask = new MyTask(runnable, after);
    	queue.put(myTask);
        locker.notify();
    }
}

运行代码后,我们发现两个线程都会进入阻塞状态,即死锁!!!为什么呢?

如果代码死锁了,一定要先拿jconsole看下线程的调用栈,明确死锁是卡死在哪行代码!
通过jconsole,我们找到死锁位置:
在这里插入图片描述
原因:假设此处是先执行42行这里的代码:先加锁,然后尝试从队列里take取队首元素。而queue是一个阻塞队列,特点就是队列为空时取值则阻塞!!!
此时扫描线程就阻塞在45行了,什么时候会解除阻塞?得有线程往队列里加元素!
主线程69行要通过schedule往里加元素,但是加元素的前提是先加锁,但是此时这个锁是被43行代码扫描线程占用着呢,schedule获取不到锁,无法执行put!!!
这不就死锁了吗?!~~

阻塞队列take操作wait的时候是释放队列内部的锁对象,这个代码中还有一个自己定义的locker对象。
两个线程两把锁~~

3.3 完整代码

import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

// 这个类表示一个任务
class MyTask implements Comparable<MyTask> {
    // 要执行的任务
    private Runnable runnable;
    // 什么时间来执行任务. (是一个时间戳)
    private long time;

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

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

class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    private Object locker = new Object();

    public MyTimer() {
        // 创建一个扫描线程.
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        // 取出队首元素
                        MyTask task = queue.take();
                        // 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
                        // 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 到点了, 改执行任务了!!
                            task.getRunnable().run();
                        } else {
                            // 还没到点
                            queue.put(task);
                            // 没到点, 就等待
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到1!");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到2!");
            }
        }, 4000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到3!");
            }
        }, 5000);
        System.out.println("开始计时");

        ArrayDeque<String> a = new ArrayDeque<>();
        a.peekLast();
    }
}

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

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

相关文章

收藏 | 自然语言处理(NLP)数据集汇总(附下载链接)

&#x1f384;&#x1f384;【自然语言处理NLP】简介 &#x1f384;&#x1f384; 自然语言处理(Natural Language Processing, NLP)是计算机科学领域与人工智能领域中的一个重要方向。它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。自然语言处理是一门…

自制CPU

CPU是计算机的核心部件&#xff0c;其发展历程就是人类文明的发展史。 随着计算机技术的不断发展&#xff0c; CPU也从一开始的功能简单&#xff0c;到现在已经变成功能强大、应用广泛的芯片。 但要想更深入了解 CPU&#xff0c;你需要知道它是如何工作以及为什么这么做的。 你…

数据结构003:有效的数独

原文链接&#xff1a;数据结构003&#xff1a;有效的数独 题目 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3…

Lecture7:随机梯度下降算法问题及解决、dropout正则化、学习率的选择、迁移学习

目录 1.随机梯度下降算法问题及解决 1.1 随机梯度下降算法SGD的问题 1.2 具有动量的梯度下降算法SGDMomentum 1.3 Nesterov加速梯度法 1.4 AdaGrad 1.5 RMSProp 1.6 融合&#xff01;Adam算法 2. 学习率的选取 3. 正则化 3.1 dropout正则化 4. 迁移学习 1.随机梯度下…

一篇文章带你了解python数据分析岗位怎么样

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 分析目标 各城市对数据分析岗位的需求情况 不同细分领域对数据分析岗的需求情况 数据分析岗位的薪资状况 工作经验与薪水的关系 公司都要求什么掌握什么技能 岗位的学历要求高吗 不同规模的…

详解C语言中的自定义类型(结构体,枚举,联合)

目录 1. 结构体 1.1 结构的声明 1.2 结构的自引用 1.3 结构体变量的定义和初始化 1.4 结构体内存对齐 (计算结构体的大小) 1.5 结构体传参 2. 位段&#xff08;结构体实现位段&#xff09; 2.1 什么是位段 2.2 位段的内存分配 2.3 位段的跨平台问题 3. 枚举 …

node.js-http模块学习

目录 1.什么是 http 模块 2.进一步理解 http 模块的作用 3.用node.js创建最基本的 web 服务器 req 请求对象&#xff1a; res 响应对象&#xff1a; 解决中文乱码问题&#xff1a; 根据不同的url响应 不同的html页面 1.什么是 http 模块 http 模块是 Node.js 官方提供的、…

3.Nacos系列之配置管理

上文中我们学习到服务的注册&#xff0c;本文我们进行服务的调用及配置管理相关的实践 1. 服务调用实践 接着上篇文章的代码&#xff0c;我们新建模块nacos-service-consumer 在pom.xml目录下新增依赖 <dependencies><dependency><groupId>org.springfra…

Bean 作用域和生命周期 · Bean 作用域存在的问题 · Bean 六种作用域 · 执行流程 · 生命周期演示

Spring 是用来存储和读取 Bean&#xff0c;因此 Spring 中 Bean 是最核心的操作资源&#xff0c;我们需要深入学习一下 Bean 对象。 一、Bean 的作用域问题1.1 原因分析1.2 作用域定义二、Bean 的六种作用域singleton 单例作用域prototype 原型作用域request 请求作用域session…

【OpenCV学习】第16课:图像边缘提取 - Sobel算子详细剖析(图像梯度)

仅自学做笔记用,后续有错误会更改 理论 卷积的应用 - 图像边缘提取&#xff1a; 边缘是什么&#xff1a;是像素值发生跃迁的地方&#xff0c; 是图像的显著特征之一&#xff0c; 再图像特征提取丶对象检测丶模式识别等方面都有重要作用如何捕捉/提取边缘&#xff1a;对图像求…

关于LuaGC算法的演化概述

2年不用&#xff0c;就忘了&#xff0c;在这记录下。 5.0版本的双色标记清除算法 此算法中&#xff0c;每个对象会有两种标记态&#xff1a;白色和黑色&#xff1b;新创建的对象都是白色 过程&#xff1a; 1.初始化阶段&#xff1a;将root链表中的所有对象放入待检链表中&am…

0204隐函数及由参数方程所确定的函数的导数相关变化率-导数与微分

1 隐函数 定义&#xff1a;设有两个非空数集A,BA,BA,B.对于∀x∈A\forall x\in A∀x∈A&#xff0c;由二元方程F(x,y)0F(x,y)0F(x,y)0对应唯一的y∈By\in By∈B,称此对应关系是二元方程F(X,y)0F(X,y)0F(X,y)0确定的隐函数。 相应的由yf(x)yf(x)yf(x)确定的对应关系称为显函数。…

vue.js:作用域插槽的使用案例

作用域插槽的使用理解 父组件替换插槽的标签&#xff0c;但是内容是由子组件提供的。 案例需求 子组件中包含一组数据&#xff0c;比如&#xff1a;pLanguages&#xff1a;[‘Java’,‘c’,‘JavaScript’,‘python’,‘C语言’,‘Go’,‘C#’]现需要在多个页面进行操作&…

19.8 适配器概念、分类、范例与总结

一&#xff1a;适配器基本概念 把一个既有的东西进行适当的改造&#xff0c;比如增加点东西&#xff0c;或者减少点东西&#xff0c;就构成了一个适配器。 三种适配器&#xff1a;容器适配器、算法适配器、迭代适配器。 二&#xff1a;容器适配器 本章第三节学习过双端队列de…

个人有效:关于VMware虚拟机开机蓝屏问题的解决

文章目录前言禁用Hyper-V等服务Device 服务等启动相关是否需要VMware最新版本电脑虚拟化问题启用或关闭windows功能的设置关于VMware虚拟机的卸载参考前言 搜了海量文章&#xff0c;实操过大部分的方法&#xff0c;一顿折腾、最后莫名其妙的能跑了…。~~两天来急痛攻心&#xf…

Hot100-寻找重复数

1 前言 给定一个包含 n 1 个整数的数组 nums &#xff0c;其数字都在 [1, n] 范围内&#xff08;包括 1 和 n&#xff09;&#xff0c;可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 &#xff0c;返回 这个重复的数 。 1.1 暴力解法 两次for循环&#xff1a…

spring——AOP面向切面编程—— 一般切面的 AOP 开发

一般切面的 AOP 开发 当我们在使用 Spring AOP 开发时&#xff0c;若没有对切面进行具体定义&#xff0c;Spring AOP 会通过 Advisor 为我们定义一个一般切面(不带切点的切面)&#xff0c;然后对目标对象(Target)中的所有方法连接点进行拦截&#xff0c;并织入增强代码。 工程依…

Pytest----测试脚本上传git代码仓库

【原文链接】Pytest----测试脚本上传git代码仓库 在企业实战中&#xff0c;自动化测试脚本也要放在代码管理平台的&#xff0c;可以选择第三方公共的git代码托管平台&#xff0c;比如github、gitee等&#xff0c;当然也可以在企业内部搭建gitlab作为代码托管平台&#xff0c;他…

蓝桥杯模拟赛习题练习(一)

题目来源&#xff1a;第十四届蓝桥杯模拟赛第一期 注&#xff1a;代码都是自己写的&#xff0c;不是参考答案&#xff01; 1. 二进制位数 问题描述&#xff1a; 十进制整数2在十进制中是1位数&#xff0c;在二进制中对应10 &#xff0c;是2位数。 十进制整数22在十进制中是2位…

Linux系统调用实现简析

1. 前言 限于作者能力水平&#xff0c;本文可能存在的谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 背景 本篇基于 Linux 4.14 ARM 32 glibc-2.31 进行分析。 3. 系统调用的实现 3.1 系统调用的发起 3.1.1 起于用户空间 我们随意挑选一个…