【多线程案例】定时器

news/2024/7/23 19:40:34

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() {@Overridepublic void run() {System.out.println("hello");}}, 3000);}

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

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

3. 手写代码实现定时器

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

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

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

//任务类 描述任务和任务的delay时间static class Task implements Comparable<Task>{//任务private Runnable command;//delayprivate long time;public Task(Runnable command,long time){this.command = command;//时间是在现在的时间的基础上加上delaythis.time = System.currentTimeMillis() + time;}public void run(){command.run();}@Overridepublic 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() {@Overridepublic void run() {System.out.println("我有第一个任务!");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("我有第二个任务!");}},1000);myTimer.schedule(new Runnable() {@Overridepublic 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;//delayprivate long time;public Task(Runnable command,long time){this.command = command;//时间是在现在的时间的基础上加上delaythis.time = System.currentTimeMillis() + time;}public void run(){command.run();}@Overridepublic 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() {@Overridepublic void run() {System.out.println("我有第一个任务!");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("我有第二个任务!");}},1000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("我有第三个任务!");}},2000);}
}

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

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

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


https://dhexx.cn/news/show-5071410.html

相关文章

Day43:VUEX

1. 基本介绍 这里介绍的VueX是匹配Vue3的V4版本&#xff0c;它绝大多数功能使用都和之前基本保持一致。 1.1 官方定义 先一起看一下官网对于VueX的定义&#xff1a; Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 库。它采用集中式存储管理应用的所有组件的状态&…

各种数据库分页查询SQL

一、DB2: DB2分页查询 SELECT * FROM (Select 字段1,字段2,字段3,rownumber() over(ORDER BY 排序用的列名 ASC) AS rn from 表名) AS a1 WHERE a1.rn BETWEEN 10 AND 20 以上表示提取第10到20的纪录 select * from (select rownumber() over(order by id asc ) as rowid from …

负载均衡原理及应用

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Leetcode168. Excel表列名称

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题解&#xff1a; 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 代码如下&#xff1a; class Solution {public String convertToTitle(int columnNumber) {StringBuild…

Shell脚本编写:从零到精通

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

《动手学深度学习 Pytorch版》 5.6 GPU

5.6.1 计算设备 import torch from torch import nntorch.device(cpu), torch.device(cuda:0) # cuda等价于cuda:0&#xff08;只有一块显卡没法试别的块号&#xff09;(device(typecpu), device(typecuda, index0))torch.cuda.device_count() # 查询可用GPU数量1def try_gp…

Python实现四维图像绘制系统

文章目录 动图绘制系统的实现播放控制接口优化总结 Python绘图系统&#xff1a; &#x1f4c8;从0开始的3D绘图系统&#x1f4c9;一套3D坐标&#xff0c;多个函数&#x1f4ca;散点图、极坐标和子图自定义控件&#xff1a;绘图风格&#x1f4c9;风格控件&#x1f4ca;定制绘图…

Apache Spark 的基本概念

Apache Spark 是一种快速、可扩展、通用的数据处理引擎。它是一种基于内存的计算框架&#xff0c;支持分布式数据处理、机器学习、图形计算等多种计算任务。与传统的 Hadoop MapReduce 相比&#xff0c;Spark 具有更高的性能和更广泛的应用场景。 Spark 中的基本概念包括&…

2817. 限制条件下元素之间的最小绝对差;2305. 公平分发饼干;878. 第 N 个神奇数字

2817. 限制条件下元素之间的最小绝对差 核心思想&#xff1a;枚举二分。我们去枚举nums[j]&#xff0c;然后用一个数据结构去装nums[i]&#xff0c;利用二分去找在这个数据结构中离nums[j]最近的值的下标&#xff0c;然后统计最小值。这个数据结构可以使用SortedList&#xff…

pg-备份和还原

1. 逻辑备份 PostgreSQL中提供了pg_dump、pg_dumpall命令进行数据库的逻辑备份。pg_dump与pg_dumpall命令的功能差不多&#xff0c;只是pg_dumpall是将一个PostgreSQL数据库集群全部转储到一个脚本文件中&#xff0c;而pg_dump命令可以选择一个数据库或部分表进行备份。使用pg_…