JavaEE(系列10) -- 多线程案例3(定时器)

news/2024/11/20 11:47:55/

目录

1. 定时器

2. 标准库中的定时器

3. 实现定时器

3.1 创建带优先级的阻塞队列 

3.2 创建MyTask类

3.3 构建schedule方法

3.4 构建timer类中的线程

3.5 思考



1. 定时器

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

2. 标准库中的定时器

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

使用步骤:

1. 实例化Timer对象

2.调用timer.schedule("任务",执行时间) 

public class timerTest {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello4");}},4000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello3");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello2");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello1");}},1000);System.out.println("hello");}
}

 运行结果:先打印主进程的hello,后面陆续按照指定的时间进行打印每个线程的内容

3. 实现定时器

定时器的构成

  1. 一个带优先级的阻塞队列 
  2. 队列中的每个元素都是一个Task对象
  3. Task 中带有一个时间属性, 队首元素就是即将执行的任务
  4. 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行

为什么使用带有优先级的堵塞队列?

答案:因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.

3.1 创建带优先级的阻塞队列 

3.2 创建MyTask类

队列中的每个元素都是一个Task对象,创建Mytask类,用来描述要执行的任务,以及执行的时间.(用于传入堵塞队列)

此处需要注意:

我们需要将Mytask类实现Comparable接口,根据执行的时间进行比较. 这样才能传入带有优先级的堵塞队列.

 最终MyTask类代码为:

class MyTask implements Comparable<MyTask>{public Runnable runnable;public long time;public MyTask(Runnable runnable, long delay){// 取当前时刻的时间戳 + delay = 当前该任务实际执行的时间戳this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}@Overridepublic int compareTo(MyTask o) {//每次取出的是时间最小的元素return (int)(this.time -o.time);}
}

3.3 构建schedule方法

通过schedule方法往队列中插入Task对象

3.4 构建timer类中的线程

Timer 类中存在一个 worke 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.

此线程的实现思路

  • 1. 线程要执行在队列中不断地取出任务  queue.take();
  • 2.取出任务要进行比较当前系统时间与任务执行时间
  • 3.如果任务执行时间小于当前系统时间,就说明要执行任务了.调用 myTask.runnable.run();
  • 4.如果当前取出的任务执行时间大于当前系统时间,就说明任务还没有到执行时间,将任务推送到队列中.同时进入堵塞等待
  •  5. 在schedule方法中,往优先级队列推送任务之后,同时加一个notify方法,用来唤醒此时正在堵塞的线程,使得堵塞等待解除,重新取队首任务进行比较时间.

 加wait notify的好处,就是work线程不需要一直进行取队首元素,这样会消耗系统资源,造成没必要的浪费,只需要等待堵塞当前距离执行任务的时间差就可以,当有新的任务添加进来的时候接触堵塞,重新进行计算时间差,再决定是否进行执行任务,还是进入堵塞状态.

public class MyTimer {private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();// 创建一个锁对象private final Object locker = new Object();public MyTimer(){//1.创建一个线程Thread work = new Thread(()->{while (true){try {synchronized (locker) {//2.队列中取出一个任务MyTask myTask = queue.take();//3 获取当前时间long curTime = System.currentTimeMillis();//4. 任务执行时间与当前时间进行对比if (myTask.time <=  curTime){//4.1 任务执行时间小于等与当前时间,说明应该要执行任务了myTask.runnable.run();}else {//4.1 任务执行时间大于等与当前时间,说明该任务还没有到执行的时间,再将刚才取出的任务放回原来的队列queue.put(myTask);locker.wait(myTask.time-curTime);//针对这个wait()://1.方便随时唤醒,比如当前时刻是14:00,约定14:30要执行上课任务,//此时取出队首元素,发现时间没有到,就wait(任务执行时间-当前时间)//2.当新的任务来了,需要比之前的队伍提前执行,那么就需要进行唤醒之前的wait(),//重新取队首元素,进行比较时间,确定wait()的时间.}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});work.start();}public void schedule(Runnable runnable, long delay){// 根据参数,构造MyTask,插入队列MyTask myTask = new MyTask(runnable,delay);queue.put(myTask);synchronized (locker){locker.notify();}}
}

3.5 思考

我们将锁加在了整个执行任务.此时我们如果只针对wait进行加锁?这样线程安全吗?不安全的话给出理由.

 

答案:会出现线程不安全的情况

比如下图解释:

        我们此时有两个线程,T1线程此时取出一个任务(执行时间为14:30),比较当前时间(14:00),还没有到执行时间,此时将任务推送给队列,但是在推送之前,此时有一个T2线程,正在插入一个新的任务(14:10),同时执行了notify操作,但是此时T1线程并没有wait,此时就空打一炮,此时T1线程开始拿到锁,进行堵塞等待,但是此时等待的时间为(14:30 - 14:00),T2线程插入的新任务还有10分钟需要执行,但是因为之前已经notify一次,此时堵塞的时间无法进行唤醒操作,所以T2线程插入的这个任务要等到14:30才能执行,这就引起线程的不安全.

 

        当我们加锁在整个(取出任务和推送任务),T1线程一定在wait之前不会使得T2线程执行notify操作,因为T1线程在加锁中,等待wait之后才会解除锁,等待T1的锁解除,T2才会执行notify操作. 进而去唤醒T1线程中的wait.

 


http://www.ppmy.cn/news/89095.html

相关文章

【GPT科技系列】国内开发者调用openAI-API科技方法

1. 前言 openAI上线7个月了&#xff0c;但是随着openAI的约束越来越多&#xff0c;国内开发者想要使用openai的接口实现开发简直就是难上加难。那真的就没有办法了吗&#xff1f;no no no&#xff0c;CF解决一切不开心~ 2.准备工作 我们需要一个国际域名 注册cloudflare账号 …

在Windows上安装Docker与k8s,完美亲测!

一、软件准备 1、去Docker官网下载Docker Desktop&#xff0c;并一键安装 2、下载k8s-for-docker-desktop包 git clone https://github.com/AliyunContainerService/k8s-for-docker-desktop.git二、镜像源配置 配置docker的国内镜像&#xff0c;国外的网络下载可能比较慢 doc…

PaLM 2重磅来袭,深挖谷歌92页技术报告亮点总结

谷歌CEO桑达尔・皮查伊&#xff08;Sundar Pichai&#xff09;亲切地将2023年称为是一个AI busy year&#xff0c;当地时间5月10日&#xff0c;谷歌IO大会上&#xff0c;谷歌大语言模型PaLM 2虽迟但到。作为一个“AI-first”公司&#xff0c;谷歌在Bard聊天机器人爆出事实性错误…

365天深度学习打卡 第P9周:YOLOv5的backbone实现

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 文章目录 一、Backbone模块代码1.1 Conv模块1.2 C3模块Bottleneck模块SPPF模块 二、数据集和相关参数设置2.1 数据集操作2.2 相关参数设置2.3…

LAMP的部署(天光渐暗,暮色里遗漏了一丝蓝,星辰便从中亮起。)

文章目录 一、LAMP架构概述二、LAMP框架搭建1.准备工作2.部署apache&#xff08;1&#xff09;安装环境依赖包&#xff08;2&#xff09;配置软件模块&#xff08;3&#xff09;服务优化&#xff08;4&#xff09;添加httpd服务&#xff08;5&#xff09;修改httpd 服务配置文件…

C语言中的数学库math.h介绍

目录 1、三角函数 2、双曲函数 3、指数函数与对数函数 4、幂函数 5、误差与伽马函数 6、四舍五入与余数函数 7、绝对值、最小、最大 Absolute、Minimum, maximum C语言中的数学函数库是math.h&#xff0c;它提供了许多常用的数学函数&#xff0c;如三角函数、指数函数、…

MySQL mysqldump备份数据库(附带实例)

数据库的主要作用就是对数据进行保存和维护&#xff0c;所以备份数据是数据库管理中最常用的操作。为了防止数据库意外崩溃或硬件损伤而导致的数据丢失&#xff0c;数据库系统提供了备份和恢复策略。 保证数据安全的最重要的一个措施就是定期的对数据库进行备份。这样即使发生…

linuxOPS基础_运维概述,及其泛概念

运维岗位定义 什么是运维&#xff1f; ​ 在技术人员&#xff08;写代码的&#xff09;之间&#xff0c;一致对运维有一个开玩笑的认知&#xff1a;运维就是修电脑的、装网线的、背锅的岗位。 ​ IT运维管理是指为了保障企业IT系统及网络的可用性、安全性、稳定性&#xff0…