在多线程中自定义实现定时器(Timer)

简介: 在多线程中自定义实现定时器(Timer)

一.前提概要:我们需要清楚理解的是:我们所实现的定时器,是对JDK中定时器的模仿和重现,与JDK提供的定时器类似,我们使用schedule()方法实现对任务的添加,使用BlockingQueue来组织任务,使用定时器的构造方法完成对阻塞队列中的任务的检查和实现(在阻塞队列中检查取出和完成任务)

二.定时器的组成部分

创建MyTask类实现对任务的定义

在MyTask中核心主要是自定义的runnable方法以及实现的时间

7013f8f36acc047c8959ad4f5e3f4429.png

除此之外我们需要自定义实现time和runnable的get()方法,以便满足后续定时器对这两个属性的调用

总体代码如下:

public class MyTask {

   private long time;

   private Runnable runnable;

//创建get任务和时间

 

   public long getTime() {

       return time;

   }

 

   public Runnable getRunnable() {

       return runnable;

   }

//创建构造方法

   public MyTask(Runnable runnable,long time){

     

       this.time=time+System.currentTimeMillis();

       this.runnable=runnable;

   }

 

}

但是如果是这样,当我们进行对该任务的测试时,我们才发现报出这样的异常:

9f5b78db14f7f39d0e7e985a5d4fcac7.png

如果大家学习过优先级队列(堆)的数据结构,我们队这个这个异常也就不陌生了:我们对自定义的实现类没有实现自定义的比较器或者comparable接口,所以为了避免这个异常,我们需要实现comparable接口,并重写compareTo()方法,(那为什么我们需要实现comparable接口呢?因为我们任务进行实现的数据结构是比较级的阻塞队列,该数据结构要求必须实现该接口或者重写比较器comparator)优化代码如下:

public class MyTask implements Comparable<MyTask>{

   private long time;

   private Runnable runnable;

//创建get任务和时间

 

   public long getTime() {

       return time;

   }

 

   public Runnable getRunnable() {

       return runnable;

   }

//创建构造方法

   public MyTask(Runnable runnable,long time){

       //判断时间的可靠性

       if(time<0){

           throw  new RuntimeException("时间错误");

       }

       this.time=time+System.currentTimeMillis();

       this.runnable=runnable;

   }

   @Override

   public int compareTo(MyTask o) {

       //第一种方式

      return (int)this.time-o.time;

   

}

我们实现了compareTo()方法,但是我们去看这个方法,我们发现它实现了一个由long到int的一个强制类型转化,尽管在绝大多数的情况下两个时间相减符合int类型的范围,但是仍存在整形变量由long强制转化为int出现的越界问题,为了避免该问题的出现,我们对compareTo()方法进行优化:

public int compareTo(MyTask o) {

       //第一种方式

     //  return this.time-o.time;

       //为了防止越界,我们选择第二种比较方式

       if(this.time>o.time){

           return 1;

       }

       if(this.time==o.time){

           return  0;

       }

       else{

           return -1;

       }

   }

创建自定义类MyTimer实现对任务的组织调用和实现(使用PriorityBlockingQueue进行对任务的组织,schedule()方法完成对任务的实现)

我们可以对任务加入阻塞队列和任务的取出的逻辑进行总结:

①将任务列入阻塞队列:直接利用put()方法放入即可

②将任务移出阻塞队列并实现:1.取出任务2.判断时间到没到,如果到了直接将任务执行,时间没有到,则将任务重新放入组赛队列即可

public class MyTimer {

   //创建锁对象

   private Object locker=new Object();

   //创建比较队列

   BlockingQueue<MyTask>queue=new PriorityBlockingQueue();

   //创建schedule方法,放入队列

   public void schedule(MyTask myTask) throws InterruptedException {

       queue.put(myTask);

   }

   //创建构造器,实现队列元素的取出

   public MyTimer(){

       //创建线程

       Thread thread=new Thread(()->{

           //创建循环

           while(true) {

               //扩大synchronized范围,保证原子性

 

               //取出元素

               try {

                   MyTask task = queue.take();

                   //判断是否到达该执行的时间

                   long time = System.currentTimeMillis();

                   if (time >= task.getTime()) {

                       //执行

                       task.getRunnable().run();

                   } else {

                       //重新放入

                       queue.put(task);

                   }

 

 

               } catch (InterruptedException e) {

                   throw new RuntimeException(e);

               }

 

           }

       });

       thread.start();

 

   }

}

我们通过这两个类事实上已经基本能实现定时器的基本功能,仔细观察,我们不难发现其中的不足:

效率问题:当我们判定任务没有到时间时,我们选择的策略是将其重新放入阻塞队列,但是如果距离到达时间相对较长的话,不全的判断和取出会增加cpu的消耗,我们应该对此进行优化:在判断没有到时间时,我们计算需要等待的时间,先让线程wait()

如果线程wait之后,在此时我们新加入了一个比之前wait()时间结束靠前的任务:这时候线程处于阻塞状态,没办法按时执行任务,(我们称这个状态为忙等状态)这时候我们则需要在加入任务时对线程进行唤醒。代码优化如下:

Thread thread=new Thread(()->{

           //创建循环

           while(true) {

               //扩大synchronized范围,保证原子性

 

               //取出元素

               try {

                   MyTask task = queue.take();

                   //判断是否到达该执行的时间

                   long time = System.currentTimeMillis();

                   if (time >= task.getTime()) {

                       //执行

                       task.getRunnable().run();

                   } else {

                       long gap = task.getTime() - time;

                       //睡眠并且重新放入,睡眠的目的在于避免cpu做无用的消耗

                       synchronized (locker) {

                           locker.wait(gap);

                       }

                       //重新放入

                       queue.put(task);

                   }

 

 

               } catch (InterruptedException e) {

                   throw new RuntimeException(e);

               }

 

           }

       });

3.这样确实对忙等问题进行了优化,但是有没有完全解决呢?事实上并没有,我们观察如下场景:


719e1c687ce3981851aaff105f0aeb94.png

这时候我们如何改良呢?其实我们不难理解,其实这个问题产生的本质在于原子性问题:而原子性的解决方案是我们扩大synchronized范围即可。

   public MyTimer(){

       //创建线程

       Thread thread=new Thread(()->{

           //创建循环

           while(true) {

               //扩大synchronized范围,保证原子性

               synchronized (locker) {

                   //取出元素

                   try {

                       MyTask task = queue.take();

                       //判断是否到达该执行的时间

                       long time = System.currentTimeMillis();

                       if (time >= task.getTime()) {

                           //执行

                           task.getRunnable().run();

                       } else {

                           long gap = task.getTime() - time;

                           //睡眠并且重新放入,睡眠的目的在于避免cpu做无用的消耗

                         

                               locker.wait(gap);

                         

                           //重新放入

                           queue.put(task);

                       }

 

 

                   } catch (InterruptedException e) {

                       throw new RuntimeException(e);

                   }

 

               }

           }

       });


831a4a9fda5322e44ce565ed14489265.png

而如果扩大了synchronized的范围之后,如果我们使用上图中的测试用例进行测试的话,我们却发现任务2无法执行,换句话说,在任务1后出现了死锁问题。


5961e8da726ff0dc3008c14623e47b95.png

4e450a2d6b3a16c3f1f4ca4da9223859.png

上面两张图片分析了死锁问题的产生原因,我们的本意是为了避免发生频率相对较小的原子性问题,却产生了问题相对较大的死锁问题,显然这样的解决方案是不合理的,所以我们必须在处理忙等问题上实现新的一种解决方案:通过创建一个新的后台线程,后台线程实现的任务也很简单:由于原子性所伴随的忙等问题是由于当一个新的任务加入阻塞队列之后出现了一次空的notifyAll()的问题,那么我们的下手点也变得明了起来了:通过增加notifyAll()的次数,折中处理出现的空notifyAll()的问题。

//创建新的线程,折中解决原子性

       Thread backGround=new Thread(()->{

           synchronized (locker){

               locker.notifyAll();

           }

           //休眠

           try {

               TimeUnit.SECONDS.sleep(1);

               //设置为后台线程

               Thread.currentThread().setDaemon(true);

           } catch (InterruptedException e) {

               throw new RuntimeException(e);

           }

       });

       backGround.start();

将这些问题解决之后,我们所手动实现的定时器已经是一个相对而言比较完善的定时器了,我们将完整的代码放在下面:

public class MyTimer {

   //创建锁对象

   private Object locker=new Object();

   //创建比较队列

   BlockingQueue<MyTask>queue=new PriorityBlockingQueue();

   //创建schedule方法,放入队列

   public void schedule(MyTask myTask) throws InterruptedException {

 

       queue.put(myTask);

       //解除睡眠

       synchronized (locker){

           locker.notifyAll();

       }

   }

   //创建构造器,实现队列元素的取出

   public MyTimer(){

       //创建线程

       Thread thread=new Thread(()->{

           //创建循环

           while(true) {

               //扩大synchronized范围,保证原子性

               synchronized (locker) {

                   //取出元素

                   try {

                       MyTask task = queue.take();

                       //判断是否到达该执行的时间

                       long time = System.currentTimeMillis();

                       if (time >= task.getTime()) {

                           //执行

                           task.getRunnable().run();

                       } else {

                           long gap = task.getTime() - time;

                           //睡眠并且重新放入,睡眠的目的在于避免cpu做无用的消耗

 

                               locker.wait(gap);

 

                           //重新放入

                           queue.put(task);

                       }

 

 

                   } catch (InterruptedException e) {

                       throw new RuntimeException(e);

                   }

 

               }

           }

       });

       thread.start();

       //创建新的线程,折中解决原子性

       Thread backGround=new Thread(()->{

           synchronized (locker){

               locker.notifyAll();

           }

           //休眠

           try {

               TimeUnit.SECONDS.sleep(1);

               //设置为后台线程

               Thread.currentThread().setDaemon(true);

           } catch (InterruptedException e) {

               throw new RuntimeException(e);

           }

       });

       backGround.start();

   }

}

public class MyTask implements Comparable<MyTask>{

   private long time;

   private Runnable runnable;

//创建get任务和时间

 

   public long getTime() {

       return time;

   }

 

   public Runnable getRunnable() {

       return runnable;

   }

//创建构造方法

   public MyTask(Runnable runnable,long time){

       //判断时间的可靠性

       if(time<0){

           throw  new RuntimeException("时间错误");

       }

       this.time=time+System.currentTimeMillis();

       this.runnable=runnable;

   }

   @Override

   public int compareTo(MyTask o) {

       //第一种方式

     //  return this.time-o.time;

       //为了防止越界,我们选择第二种比较方式

       if(this.time>o.time){

           return 1;

       }

       if(this.time==o.time){

           return  0;

       }

       else{

           return -1;

       }

   }

}


相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
221 0
|
7月前
|
Java 数据库连接 调度
面试题:用过线程池吗?如何自定义线程池?线程池的参数?
字节跳动面试题:用过线程池吗?如何自定义线程池?线程池的参数?
106 0
|
7月前
多线程案例-定时器(附完整代码)
多线程案例-定时器(附完整代码)
306 0
|
7月前
多线程(初阶八:计时器Timer)
多线程(初阶八:计时器Timer)
114 0
|
7月前
|
安全 Java
Java多线程基础-10:代码案例之定时器(一)
`Timer` 是 Java 中的一个定时器类,用于在指定延迟后执行指定的任务。它常用于实现定时任务,例如在网络通信中设置超时或定期清理数据。`Timer` 的核心方法是 `schedule()`,它可以安排任务在延迟一段时间后执行。`
149 1
|
2月前
|
安全 Java
【多线程-从零开始-拾】Timer-定时器
【多线程-从零开始-拾】Timer-定时器
35 0
|
4月前
|
存储 Java 开发者
HashMap线程安全问题大揭秘:ConcurrentHashMap、自定义同步,一文让你彻底解锁!
【8月更文挑战第24天】HashMap是Java集合框架中不可或缺的一部分,以其高效的键值对存储和快速访问能力广受开发者欢迎。本文深入探讨了HashMap在JDK 1.8后的底层结构——数组+链表+红黑树混合模式,这种设计既利用了数组的快速定位优势,又通过链表和红黑树有效解决了哈希冲突问题。数组作为基石,每个元素包含一个Node节点,通过next指针形成链表;当链表长度过长时,采用红黑树进行优化,显著提升性能。此外,还介绍了HashMap的扩容机制,确保即使在数据量增大时也能保持高效运作。通过示例代码展示如何使用HashMap进行基本操作,帮助理解其实现原理及应用场景。
66 1
|
4月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
5月前
|
Java Spring 容器
Spring boot 自定义ThreadPoolTaskExecutor 线程池并进行异步操作
Spring boot 自定义ThreadPoolTaskExecutor 线程池并进行异步操作
256 3
|
4月前
|
Java UED
基于SpringBoot自定义线程池实现多线程执行方法,以及多线程之间的协调和同步
这篇文章介绍了在SpringBoot项目中如何自定义线程池来实现多线程执行方法,并探讨了多线程之间的协调和同步问题,提供了相关的示例代码。
1195 0