【多线程-从零开始-拾】Timer-定时器

简介: 【多线程-从零开始-拾】Timer-定时器

定时器相当于是一个闹“闹钟”

在代码中,也经常需要“闹钟”机制

网络通信中,经常需要设定一个“超时时间”

方法 作用
void schedule(TimerTask task, long delay) 指定delay时间之后(单位毫秒)执行任务task

基本效果

使用Java标准库中的Timer,在 3s 后打印“hello”:

import java.util.Timer;  
import java.util.TimerTask;  
  
public class Demo4 {  
    public static void main(String[] args) {  
        Timer timer = new Timer();  
        timer.schedule(new TimerTask() {  
            @Override  
            public void run() {  
                System.out.println("hello");  
            }        
        },3000);  
        System.out.println("程序开始执行");  
    }
}

还可以制定多个任务:

import java.util.Timer;  
import java.util.TimerTask;  
  
public class Demo4 {  
    public static void main(String[] args) {  
        Timer timer = new Timer();  
        
        timer.schedule(new TimerTask() {  
            @Override  
            public void run() {  
                System.out.println("hello1");  
            }        
        },1000);  
        
        timer.schedule(new TimerTask() {  
            @Override  
            public void run() {  
                System.out.println("hello2");  
            }        
        },2000);
          
        timer.schedule(new TimerTask() {  
            @Override  
            public void run() {  
                System.out.println("hello3");  
            }        
        },3000);  
        
        System.out.println("程序开始执行");  
    }
}

定时器的实现

对于定时器来说

  1. 创建类,描述一个要执行的任务是什么
  • 内容
  • 时间
  1. 创建多个任务,通过一定的数据结构,把多个任务存起来
  2. 有专门的线程,执行这里任务

创建一个任务

  • schedule 的时候,指定的时间是“delay”值,但是,描述任务的时候,不建议使用 delay 来表示,最好使用“绝对时间”(时间戳)来表示
  • 那之后就很容易判断当前任务是否要执行,可以随时对比当前的时间戳和记录的时间戳,看是否达到了
  • 如果当前时间戳 >记录的时间戳,那时间就到了,若 <,时间就还没到
  • 如果你写了一个delay的话,就不好对比了
  • 这样就没有了参照物,没有了比较对象
  • 比如说你写了一个 delay=5000,但你不知道什么时候是这个时间(刻舟求剑)
//定时器的任务  
class MyTimerTask {  
    //描述任务是什么  
    private Runnable runnable;  
    //通过毫秒时间戳,表示这个任务具体啥时候执行  
    private long time;  
    
    public MyTimerTask(Runnable runnable, long delay) {  
        this.runnable = runnable;  
        //获取当前时间戳 + delay,得到一个绝对的时间戳  
        this.time = System.currentTimeMillis() + delay;  
    }
  
  public void run() {  
      runnable.run();  
  }  
  
  public long getTime() {  
      return time;  
  }
}
  • 若此时为 12:00,设置的 delay1 h,那么此时的 time 就为 13:00

创建多个任务

数据结构的选择

此时我们最先需要做的就是确定是用什么数据结构来存放多任务

  • List
  • 不是一个好的选择,比较低效
  • 后续执行列表中的任务的时候,就需要依次遍历每个元素。执行完毕还需要把对应的任务从 List 中删掉
  • 可以高效方便地找到“最小/第二小/第三小”的值,而我们的定时器就是按照时间顺序来执行任务的。
  • 这样我们只要确定好时间最小的任务,判定它是否到时间需要执行了即可。若时间最小的任务都还没到时间,那其他任务就更还没到了

因为使用的是优先级队列,所以要指定出比较规则,才能排出优先级

  • MyTimeTask 类中实现 Comparable 接口
class MyTimerTask implements Comparable<MyTimerTask>{  
    ...
    ...
    ...
     
    @Override  
    public int compareTo(MyTimerTask o) {    
        return (int)(this.time - o.time);  
    }
}
class MyTimer {  
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();  
}
  • 上面comparaTo里作差的顺序就决定了是大堆还是小堆
  • 这里我们需要的是小堆
  • 这里是谁减谁,不要背,可以先写成一个顺序,试试就知道了

多线程的执行

此时,各元素可以被顺利地添加到这个优先级队列中了,各个任务已经可以被我们用优先级队列管理起来了

之后我们就需要考虑得有人去执行这里面的任务

  • 就是说,现在我们已经有了队列,得有专门的线程去这个队列里面取元素,然后去执行里面的任务
  • 并且执行之前,在取元素的时候还需要判定时间,看时间是不是符合我们的要求
class MyTimer {  
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();  
  
    public MyTimer() {  
        //创建线程,负责执行上述队列中的内容  
        Thread t = new Thread(() -> {  
            //我们也不知里面有多少个元素,就需要不停地循环去取  
            while(true){  
                //在队列不为空的情况下,取出对首元素  
                if(queue.isEmpty()){ 
                  continue;  
                }
                MyTimerTask current = queue.peek();  
                if(System.currentTimeMillis() >= current.getTime()){  
                    //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑  
                    current.run();  
                    //把执行过的任务,从队列中删除  
                    queue.poll();  
                }else {  
                    //不执行任务  
                    continue;  
                }            
            }        
        });   
        t.start(); 
    }
}

System.currentTimeMillis() >= current.getTime()

  • 比如,当前时间是10:30,任务时间是12:00,不应该执行
  • 直接跳过这次循环
  • 如果当前的时间是10:30,任务时间是10:29,就应该执行
  • 先执行 runnable 中的 run 方法,随后使用 poll 将这个元素从队列中删去
  • 在这个循环中,首先取到的是时间最靠前的任务(因为是小堆排序),再取就是第二靠前的任务

之后我们就需要给定时器里面安排任务,实现 schedule 方法

class MyTimer {  
    ...
    ...
    ...
    
    public void schedule(Runnable runnable, long delay) {  
        MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);  
        queue.offer(myTimerTask);  
    }
}
  • 实例化一个 MyTimeTask 对象,参数为所要执行的具体任务和时间,随后将其添加到队列中

特别注意

class MyTimer {  
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();  
  
    public MyTimer() {  
        //创建线程,负责执行上述队列中的内容  
        Thread t = new Thread(() -> {  
            //我们也不知里面有多少个元素,就需要不停地循环去取  
            while(true){  
                //在队列不为空的情况下,取出对首元素  
                if(queue.isEmpty()){ 
                  continue;  
                }
                MyTimerTask current = queue.peek();  
                if(System.currentTimeMillis() >= current.getTime()){  
                    //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑  
                    current.run();  
                    //把执行过的任务,从队列中删除  
                    queue.poll();  
                }else {  
                    //不执行任务  
                    continue;  
                }            
            }        
        });   
        t.start(); 
    }
}

对于此处的 while 循环

  1. 若初始情况下,队列中没有任何元素
  • 此处的逻辑就会在短时间内进行大量的循环,并且这些循环都是没什么意义的,就类似于“线程饿死
  • 所以,我们就将这里的 continue 操作改为 wait/notify 操作。在空的时候 wait,在不空的时候 notifyschedule 之后)
  • 这样,如果队列是空的,就会进行 wait,避免无意义的循环,直到进行 schedule 操作之后,将其 notify
  1. while里面的判空
  • if 改为 while 更加安全
  • 避免 wait 被一些其他的方式唤醒了,结果队列还是为空,往下走进行 peek 操作,就会出现问题
  • 改为 while 后,即使被意外唤醒了,也能够继续确认,是不是要继续 wait
  1. 假设队列中,已经包含一些元素,当前时间是10:45,任务时间是12:00
  • 这样,线程也会一直循环执行,检查时间到没到:没到就 continue,然后继续进行循环判定
  • 类似于:我定了一个 12:00 的闹钟,此时我一看是 10:45,我继续睡一会,刚一闭眼,又睁眼看时间,为 10:45,我又闭眼;刚一闭眼,我又睁眼看时间…
  • 这种情况下,并没有完成什么实质性的工作,但还要一直进行循环,也类似于“线程饿死
  • 所以,此时我们也将这里的 continue 改为 wait,但此时就不需要用 notify 进行唤醒了,我们可以指定一个“超时时间”(当前时间距离任务时间还有多久)
  • 注意:此时不应该使用sleep,因为可能存在这样的情况:
  1. 你在 sleep 的过程中,新来了一个时间更早的任务,但线程无法被提前唤醒;若使用 wait 的话,每次在 schedule 的时候都会进行 notify 将线程唤醒,让线程再次进行判断,重新设置等待时间,这样就不会错过新的任务
  2. sleep 在休眠的时候是不会释放锁的,这样就会造成进行取出和删除操作的线程是抱着锁睡的,之后 schedule 就拿不到锁了,就进行不了新增任务的操作了
    我们可以将两个 wait 的异常 try-catch 一起放在外面

改后:

class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private static Object locker = new Object();
    public MyTimer() {
        //创建线程,负责执行上述队列中的内容
        Thread t = new Thread(() -> {
            try {
                //我们也不知里面有多少个元素,就需要不停地循环去取
                while (true) {
                    synchronized (locker) {
                        //在队列不为空的情况下,取出对首元素
                        while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTimerTask current = queue.peek();
                        if (System.currentTimeMillis() >= current.getTime()) {
                            //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑
                            current.run();
                            //把执行过的任务,从队列中删除
                            queue.poll();
                        } else {
                            //不执行任务
                            locker.wait(current.getTime() - System.currentTimeMillis());
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t.start();
    }
    public void schedule(Runnable runnable, long delay) {
        MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
        synchronized (locker) {
            queue.offer(myTimerTask);
            locker.notify();
        }
    }
}

线程安全问题

在定时器里面,我们在这里涉及到的核心是 Queue 这个数据结构,

在这个 Queue 这里,我们有一个专门的线程,从队列里面取元素,删除元素

然后我们还有一个 schedule 方法,去执行插入队列操作

  • 此时我们发现,schedule 里面的插入操作是一个线程,取和删操作是另一个线程
  • 那么此时存在多个线程同时修改 Queue那就肯定是存在线程安全风险
  • 尤其是我们用的还是 PriorityQueue,这个类自身不带线程安全的控制能力
    所以,此时就一定存在线程安全的风险

为了解决这个线程安全问题,我们就给操作都加上锁

class MyTimer {  
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();  
    private static Object locker = new Object();  
  
    public MyTimer() {  
        //创建线程,负责执行上述队列中的内容  
        Thread t = new Thread(() -> {  
            //我们也不知里面有多少个元素,就需要不停地循环去取  
            while (true) {  
                synchronized (locker) {  
                    //在队列不为空的情况下,取出对首元素  
                    if (queue.isEmpty()) continue;  
                    MyTimerTask current = queue.peek();  
                    if (System.currentTimeMillis() >= current.getTime()) {  
                        //执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑  
                        current.run();  
                        //把执行过的任务,从队列中删除  
                        queue.poll();  
                    } else {  
                        //不执行任务  
                        continue;  
                    }                
                }            
            }        
        });        
        t.start();  
    }    
  public void schedule(Runnable runnable, long delay) {  
        MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);  
        synchronized (locker) {  
            queue.offer(myTimerTask);  
        }    
    }
}

执行效果

主程序:

public class Demo {  
    public static void main(String[] args) {  
        MyTimer myTimer = new MyTimer();  
        myTimer.schedule(() -> {  
            System.out.println("hello 3000");  
        },3000);  
        
        myTimer.schedule(() -> {  
            System.out.println("hello 2000");  
        },2000);  
        
        myTimer.schedule(() -> {  
            System.out.println("hello 1000");  
        },1000);  
        
        System.out.println("程序开始执行");  
    }
}
//执行结构
程序开始执行
hello 1000
hello 2000
hello 3000


相关文章
|
6月前
多线程案例-定时器(附完整代码)
多线程案例-定时器(附完整代码)
300 0
|
6月前
多线程(初阶八:计时器Timer)
多线程(初阶八:计时器Timer)
107 0
|
6月前
|
安全 Java
Java多线程基础-10:代码案例之定时器(一)
`Timer` 是 Java 中的一个定时器类,用于在指定延迟后执行指定的任务。它常用于实现定时任务,例如在网络通信中设置超时或定期清理数据。`Timer` 的核心方法是 `schedule()`,它可以安排任务在延迟一段时间后执行。`
136 1
|
3月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
6月前
|
设计模式 安全 Java
多线程(代码案例: 单例模式, 阻塞队列, 生产者消费者模型,定时器)
多线程(代码案例: 单例模式, 阻塞队列, 生产者消费者模型,定时器)
58 2
|
6月前
|
设计模式 安全 Java
Java 多线程系列Ⅳ(单例模式+阻塞式队列+定时器+线程池)
Java 多线程系列Ⅳ(单例模式+阻塞式队列+定时器+线程池)
|
5月前
|
安全 Java 容器
线程池,定时器以及阻塞队列(生产者/消费者模型)
线程池,定时器以及阻塞队列(生产者/消费者模型)
40 0
|
6月前
|
消息中间件 缓存 Java
【多线程学习】深入探究定时器的重点和应用场景
【多线程学习】深入探究定时器的重点和应用场景
121 1
|
6月前
|
Java 调度
Java多线程基础-10:代码案例之定时器(二)
Java 多线程基础中,定时器(Timer)的实现通常使用 `PriorityBlockingQueue` 和 `wait/notify` 机制来管理任务。
46 0
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
44 1
C++ 多线程之初识多线程