线程的定时器问题

news/2024/11/30 18:43:12/

文章目录

  • 前言
  • 一.定时器
  • 二.定时器的具体实现和原理
    • 2.1 定时器的主要构成部分
    • 2.2 定时器的各个功能的实现
      • 任务对象
      • 任务队列
      • 定时器线程
      • 定时器线程出现的问题
  • 三.全部代码


前言

多线程的定时器是一种在多线程环境下实现定时任务的技术。它能够让多个线程在指定的时间点执行特定的任务,而不需要每个线程都自己实现一个定时器。使用多线程的定时器可以避免线程阻塞和任务调度的复杂性,提高代码的可读性和可维护性。

在多线程的定时器中,一般通过一个定时器线程来维护所有的定时任务,该线程负责按照指定的时间间隔检查所有的任务,并在任务的执行时间点将任务放入一个线程池中,让线程池中的线程来执行任务。

多线程的定时器可以通过Java内置的Timer类来实现,也可以使用第三方框架,如Quartz等。需要注意的是,定时器的任务应该是非阻塞的,以避免任务执行时间过长导致其他任务无法及时执行的问题。


一.定时器

什么是定时器呢?定时器就是定时的去安排一些任务去执行,如果大家都不了解定时器的操作的话,我们就拿生活举例子吧,定时器就是我们的闹钟,时间到了,就会去执行一些事情.
这就是定时器,当然了,我们java也提供了定时器的API方法,我们可以来看一下具体的操作.
定时器的api说明如下:
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒).
代码入下:

import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;public class ThreadDemo26 {/*定时器的操作*/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");}}, 2000);System.out.println("hello0");}
}

在这里插入图片描述
从这里可以看出,定时器在完成任务以后,我们其实并不会立即停止任务.
这说明了,定时器执行完任务后并不会结束程序,而是在那等待。因为一个定时器可能不止安排一个任务,所以一个定时器执行玩所有任务后会进入待机状态。


二.定时器的具体实现和原理

看完了,我们java的api操作的定时器,我们就来想想看,我们如果要自己实现一个定时器的时候,我们具体要做些什么呢?或者说怎么去实现呢?接下来就让我们一一去做这件事情.

2.1 定时器的主要构成部分

1.任务对象:具体的任务对象,包含了具体的任务和执行任务的时间
2.任务队列:用来存储需要执行的定时任务,每个任务都有一个指定的执行时间。
3.定时器线程:一个专门用来管理任务队列的线程,负责在任务到期时执行任务。

2.2 定时器的各个功能的实现

任务对象

具体的任务对象里面有具体的任务和执行任务的时间.具体的代码如下:

class MyTask{//具体的任务public Runnable runnable;//任务执行的具体时间,这里为了时间统一标准,我们使用绝对的时间戳public long time;public MyTask(Runnable runnable,long delay){//取当前的时间戳+delay//就作为任务执行的实际时间this.runnable=runnable;this.time=System.currentTimeMillis() + delay;}}

说明一下时间的表示,取现在的时间+给出的几秒之后的时间,这样做的目的就是为了,给出任务具体的执行时间.

任务队列

用来存储需要执行的定时任务,每个任务都有一个指定的执行时间。另外最重要的是我们需要的一个优先级带阻塞功能的队列,才能实现这样的操作.
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带
优先级的队列就可以高效的把这个 delay 最小的任务找出来.
具体的实现代码如下:

class  MyTimer{// 这个结构, 带有优先级的阻塞队列. 核心数据结构private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//插入任务对象public void schedule(Runnable runnable, long delay) {MyTask myTask=new MyTask(runnable,delay);queue.offer(myTask);}}

上述的代码就展示了,我们放任务的一个容器.我们使用了java的Api的优先的阻塞队列容器来装这些任务.

定时器线程

    public  MyTimer(){Thread thread =new Thread(() ->{
//            // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
//            while (queue.isEmpty()){
//            }while (true){MyTask myTask= null;try {myTask = queue.take();long curTime=System.currentTimeMillis();if (myTask.time <=curTime){//时间到了,可以开始执行任务了myTask.runnable.run();}else {//时间还没到的话,我们就重写塞回队列中queue.put(myTask);}} catch (InterruptedException e) {e.printStackTrace();}}});}

这里就是Timer类构造方法,具体执行任务的一些细节,逻辑大概就是:
1.我们要用一个线程去执行任务
2.如果到达时间我们就执行.
3.如果没有达到时间,我们就把取出的任务放回去
到这里,我们定时器的模拟就已经实现了.
但实际上是有些小问题的,但是这俩个小问题,就会导致非常严重的一个问题,我接下来就会来列举出我们出现的问题.

定时器线程出现的问题

第一个问题:就是没有比较器
第二个问题:存在盲等的问题


先来解决第一个问题,为什么说缺少比较器,因为我们任务出队,想的是要出最任务时间最早的任务,但是在我们在任务入队的时候,我们怎么去判断呢?你会发现我们任务对象没有提供比较方法,这个时候,我们可以加一个比较方法如下:

//任务类对象
class MyTask implements Comparable<MyTask>{//具体的任务public Runnable runnable;//任务执行的具体时间,这里为了时间统一标准,我们使用绝对的时间戳public long time;public MyTask(Runnable runnable,long delay){//取当前的时间戳+delay//就作为任务执行的实际时间this.runnable=runnable;this.time=System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTask o) {return (int)(this.time - o.time);}
}

为什么会存在盲等的问题?现在来思考?

    public  MyTimer(){Thread thread =new Thread(() ->{
//            // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
//            while (queue.isEmpty()){
//            }while (true){MyTask myTask= null;try {myTask = queue.take();long curTime=System.currentTimeMillis();if (myTask.time <=curTime){//时间到了,可以开始执行任务了myTask.runnable.run();}else {//时间还没到的话,我们就重写塞回队列中queue.put(myTask);}} catch (InterruptedException e) {e.printStackTrace();}}});}

在这段代码中,如果队列为空,while循环就会一直运行,直到队列中有任务加入,才能继续往下执行。这样的写法会导致线程进入盲等状态,浪费CPU资源。
再来另外一种情况,我们假设我们有一个任务14:30执行,但中途过程中,我们加入14:10分有个任务,14;20也有一个任务,按照我们说法就是,我们14:30的任务如果一直没有到达时间,我们这个线程就一直陷入一个等待的状态,知道14:30的时间一直到来的时候,才会执行任务,就会导致线程盲目等待,但我们要解决这个问题.就是给这线程加锁,然后让它在一定时间内被唤醒去执行其他的任务,这就是我们解决问题的目的性.
代码入下:

class  MyTimer{// 这个结构, 带有优先级的阻塞队列. 核心数据结构private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//创建一个锁对象private Object locker=new Object();//插入任务对象public void schedule(Runnable runnable, long delay) {//1.插入对象的时候,我们可以针对这个环节上锁synchronized (locker){MyTask myTask=new MyTask(runnable,delay);queue.offer(myTask);locker.notify();}}//构造方法.这里就负责执行具体的任务了public  MyTimer(){Thread thread =new Thread(() ->{
//            // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
//            while (queue.isEmpty()){
//            }while (true){try {synchronized (locker){while (queue.isEmpty()){locker.wait();}MyTask myTask= queue.peek();long curTime=System.currentTimeMillis();if (myTask.time <=curTime){//时间到了,可以开始执行任务了queue.poll();myTask.runnable.run();}else {//时间还没到的话,我们就重写塞回队列中locker.wait(myTask.time - curTime);}}// 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();}}

这里其实加锁的方式我们,很容易就能想到,就是我们想保持,程序的线程安全,就去看看,我们哪些对数据做了更改操作,其实这里不难看出,有俩个操作,如下:
1.对队列的插入操作
2.对队列的弹出操作.
第一个插入队列的操作线程加锁如下:

//插入任务对象public void schedule(Runnable runnable, long delay) {//1.插入对象的时候,我们可以针对这个环节上锁synchronized (locker){MyTask myTask=new MyTask(runnable,delay);queue.offer(myTask);locker.notify();}}

第二种弹出队列的操作加锁

synchronized (locker){while (queue.isEmpty()){locker.wait();}MyTask myTask= queue.peek();long curTime=System.currentTimeMillis();if (myTask.time <=curTime){//时间到了,可以开始执行任务了queue.poll();myTask.runnable.run();}else {//时间还没到的话,我们就重写塞回队列中locker.wait(myTask.time - curTime);}}

另外还有最重要的wait和notify的结合,我们简单地说一下
当向定时器中添加任务时,使用了 synchronized 关键字来保证线程安全,然后使用 notify() 方法通知等待的线程有新任务添加,从而唤醒等待的线程。在定时器线程中,使用 wait() 方法使线程进入等待状态,直到有新任务添加时,调用 notify() 方法唤醒该线程。同时,在执行任务时,也使用了 wait() 方法,让线程等待一定时间后再次检查是否可以执行任务。这样可以避免线程的盲等状态,提高了效率。

三.全部代码

import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;//任务类对象
class MyTask implements Comparable<MyTask>{//具体的任务public Runnable runnable;//任务执行的具体时间,这里为了时间统一标准,我们使用绝对的时间戳public long time;public MyTask(Runnable runnable,long delay){//取当前的时间戳+delay//就作为任务执行的实际时间this.runnable=runnable;this.time=System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTask o) {return (int)(this.time - o.time);}
}
//任务队列的对象
class  MyTimer{// 这个结构, 带有优先级的阻塞队列. 核心数据结构private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//创建一个锁对象private Object locker=new Object();//插入任务对象public void schedule(Runnable runnable, long delay) {//1.插入对象的时候,我们可以针对这个环节上锁synchronized (locker){MyTask myTask=new MyTask(runnable,delay);queue.offer(myTask);locker.notify();}}//构造方法.这里就负责执行具体的任务了public  MyTimer(){Thread thread =new Thread(() ->{
//            // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
//            while (queue.isEmpty()){
//            }while (true){try {synchronized (locker){while (queue.isEmpty()){locker.wait();}MyTask myTask= queue.peek();long curTime=System.currentTimeMillis();if (myTask.time <=curTime){//时间到了,可以开始执行任务了queue.poll();myTask.runnable.run();}else {//时间还没到的话,我们就重写塞回队列中locker.wait(myTask.time - curTime);}}// 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();}}public class ThreadDemo27 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello4");}}, 4000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello3");}}, 3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello2");}}, 2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello1");}}, 1000);System.out.println("hello0");}
}

执行效果:
在这里插入图片描述


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

相关文章

HCIP笔记四

重发布和路由策略 重发布 在同一个网络拓扑结构中&#xff0c;如果存在多种不同的路由协议&#xff0c;由于不同的路由协议对于路由项的处理机 制不同&#xff0c;这就会导致在网络中造成路由信息的隔离。而在路由协议的边界设备上&#xff0c;将某种路由协议的路 由信息引入另…

人工智能和5G相结合会碰撞出怎样的火花?

将第五代蜂窝技术和人工智能技术相结合是一个非常完美的例子&#xff0c;它有助于当今的创新者将两个独立的概念合作并应用于开发新的案例&#xff0c;并完善过去的创造&#xff0c;以更好地满足未来的需求。 随着新技术的不断涌现&#xff0c;越来越多的技术被合并和组合&…

用一个生动而形象的例子描述死锁的必要条件之不好意思, 我要扳手, 不要班花

不好意思, 我要扳手, 不要班花 &#x1f6e9;️死锁的必要条件生动而形象的例子Java 中的死锁示例死锁的必要条件 判断死锁的必要条件通常包括以下几点&#xff1a; 互斥条件&#xff1a;资源只能被一个进程&#xff08;或线程&#xff09;占有&#xff0c;无法被其他进程共享…

非光滑优化systune、LMI、μ综合方法

鲁棒控制中的非光滑优化systune、LMI、μ综合方法都是常用的设计方法,它们各自具有优缺点,可以根据具体问题的需求进行选择。 非光滑优化systune方法:这种方法适用于一些非线性系统的控制设计,可以自动地搜索控制器的参数,以最小化所选性能指标,如控制系统的稳定裕度、鲁…

【动手学习深度学习-----自然语言处理:预训练】

词嵌入&#xff08;Word2vec&#xff09; word2vec工具包含两个模型&#xff0c;即跳元模型&#xff08;skip-gram&#xff09;和连续词袋&#xff08;CBOW&#xff09;&#xff0c;对于在语义上有意义的表示&#xff0c;它们的训练依赖于条件概率&#xff0c;条件概率可以被看…

RabbitMQ入门

一、RabbitMQ介绍 1.1 引言 1.模块之间的耦合度太高&#xff0c; 导致一个模块宕机后&#xff0c;全部功能都不能用 2.同步通讯的成本问题 1.2RabbitMQ的介绍 市面上比较火爆的几款MQ&#xff1a; ActiveMQ&#xff0c; RocketMQ&#xff0c; Kafka&#xff0c;RabbinMQ …

ChatGPT惨遭围剿?多国封杀、近万人联名抵制……

最近&#xff0c;全世界燃起一股围剿ChatGPT的势头。由马斯克、图灵奖得主Bengio等千人联名的“暂停高级AI研发”的公开信&#xff0c;目前签名数量已上升至9000多人。除了业内大佬&#xff0c;欧盟各国和白宫也纷纷出手。 最早“动手”的是意大利&#xff0c;直接在全国上下封…

元宇宙与网络安全

元宇宙是一种虚拟现实空间&#xff0c;用户可以在计算机生成的环境中进行互动。元宇宙的应用范围很广&#xff0c;比如房地产&#xff0c;医疗&#xff0c;教育&#xff0c;军事&#xff0c;游戏等等。它提供了更具沉浸感的体验&#xff0c;更好地现实生活整合&#xff0c;以及…