在多线程中自定义实现定时器(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;

       }

   }

}


相关文章
|
1月前
多线程案例-定时器(附完整代码)
多线程案例-定时器(附完整代码)
|
3月前
|
设计模式 消息中间件 安全
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
34 1
|
19天前
|
NoSQL Java Redis
Java自定义线程池的使用
Java自定义线程池的使用
|
3月前
|
前端开发 Java BI
自定义线程池+countdownlatch
自定义线程池+countdownlatch
21 0
|
3月前
|
存储 Java
【JavaEE】多线程案例-定时器
【JavaEE】多线程案例-定时器
|
3月前
|
设计模式 存储 Java
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)
93 1
|
3月前
|
设计模式 存储 安全
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)
38 2
|
3月前
|
设计模式 Java 关系型数据库
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(一)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(一)
30 0
|
8月前
|
Java
java202303java学习笔记第三十九天自定义线程池详解1
java202303java学习笔记第三十九天自定义线程池详解1
27 0
|
8月前
|
安全 Java 调度
多线程代码案例--实现定时器
多线程代码案例--实现定时器