Java多线程基础-10:代码案例之定时器(一)

简介: `Timer` 是 Java 中的一个定时器类,用于在指定延迟后执行指定的任务。它常用于实现定时任务,例如在网络通信中设置超时或定期清理数据。`Timer` 的核心方法是 `schedule()`,它可以安排任务在延迟一段时间后执行。`

定时器就是一个闹钟。它可以设定一个时间,当时间到,就可以执行某个指定的代码。


定时器是实际开发中的一种非常常用的组件。比如网络通信中,如果对方 500ms 内没有返回数据,则要求断开连接尝试重连;又比如一个 Map,希望里面的某个 key 在 3s 之后过期(自动删除)。类似于这样的场景就需要用到定时器。


Java标准库(java.util)中提供了一个定时器类:Timer。Timer 类的核心方法为 schedule()。


一、Timer类的使用


下面是使用Timer类创建定时器的代码示例,通过创建Timer对象timer,与调用timer的schedule()方法完成定时器的设定。


1.import java.util.Timer;
import java.util.TimerTask;
 
public class Test {
    public static void main(String[] args) {
        //1-创建Timer对象
        Timer timer = new Timer();
        //2-设定定时效果:5秒
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello");
            }
        },5000);
        System.out.println("BBB");
    }
}


运行代码结果如下:立即执行System.out.println("BBB");语句打印出BBB,等待5s后,执行定时器schedule()中设定的语句,打印"hello"。



1、schedule()方法


schedule()方法的作用是在某个特定的时间后,安排执行某个指定的操作。它有多个重载:



这些重载的共同点是,第一个参数为被安排的任务task,第二个参数(或后面的所有参数共同)表示设定的任务将要执行的时间。


以schedule(TimerTask task, long delay)为例,它的两个参数分别是:


  • task:被安排的任务。
  • delay:在任务执行之前的等待时间(毫秒数)。


Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);


这里的第一个参数TimerTask,本质上就是一个Runnable。查看TimerTask的源码可以发现,它其实是一个实现了Runnable接口的抽象类。因此,当创建TimerTask对象时,也需要重写实现run()方法。run()方法中定义的行为,表示任务具体要干什么。


(new TimerTask() { ... }:创建一个匿名内部类,继承自TimerTask,并重写了run()方法。)



程序启动后,立即打印出BBB,但未立即执行计时器timer中的代码。等待5秒后,程序打印hello。


这里有一个小细节:打印完毕hello后,程序并未终止退出。这是因为Timer里内置的前台线程阻止了当前进程结束。事实上,run方法的执行正是靠Timer内部的线程控制在时间到了之后执行的。(后面会讲解如何自己实现一个Timer类,Timer内部的线程是如何控制定时的,在那里会有只管的感受。)


2、定时器的应用场景


定时器的应用场景很多,尤其是在网络编程中。


实际中,很多的“等待”并不是无期限的,而是应当指定一个超时时间。比如打开浏览器,访问CSDN。如果此时的网络状况不是很好,那么加载的时间就可能会非常久。所以,浏览器会设置一个超时时间,如果达到了当前的超时时间还没有等待结果,就会提醒用户不要再等待下去了(504 gateway time out)。


3、用Timer管理多个任务


定时器内部可以管理很多个任务。如以下代码中,同时给定时器注册5个任务:


import java.util.Timer;
import java.util.TimerTask;
 
public class Test {
    public static void main(String[] args) {
        //1-创建定时器对象timer
        Timer timer = new Timer();
 
        //2-多次调用schedule()方法,给定时器注册多个任务
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello5");
            }
        },5000);
 
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello4");
            }
        },4000);
 
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello3");
            }
        },3000);
 
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);
 
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000);
 
        System.out.println("hello0");
    }
}


运行程序后,立即打印hello0,之后每隔一秒进行一次打印。



这里就像传说中时间复杂度为O(0)的休眠排序法(笑)(手动滑稽)


二、代码实现Timer


代码实现Timer主要要考虑到两个问题:


a. 如何实现Timer对象可以同时管理多个任务?


虽然任务有很多,但它们的触发事件是不同的。因此,只需要有一个或一组工作线程来负责每次找这些任务中时间最先达到的即可。


换句话说,一个线程先执行最早的的任务,做完了之后再执行第二早的任务,以此类推。而所有任务中,时间到了的就执行,没到的就再等等。


如何实现“每次找到这些任务中时间最先到达的任务”?——堆!


堆heap是这里要用到的核心数据结构!(排序的效率要低于堆,且插入新的元素时,要维持原序列的规律比较困难。)


Java标准库中就提供了 PriorityQueue.


b. 定时器可能是有多个线程在执行 schedule 方法,那么就希望在多线程下操作 PriorityQueue 还能线程安全。


要保证线程安全,就要更进一步:Java标准库中提供了带优先级的阻塞队列,能够解决线程安全这个问题:PriorityBlockingQueue


通过查看优先级阻塞队列的源码可知,它遵循和优先级队列一样的顺序。可以根据任务的执行时间来建小根堆。不断取队头元素,取出的就是其中最小的元素。需要注意的是,该结构只有阻塞的入队列和阻塞的出队列,没有阻塞的查看队头元素方法。


1、实现思路


a.初步代码


首先创建一个MyTimer来模拟Timer,是一个自定义的定时器类。该类中封装有核心数据结构PriorityBlockingQueue。另外,创建一个类MyTask用于描述一个要执行的任务。一个要执行的任务中包含两方面信息,即:具体要做什么+什么时候做。


要特别注意的是,为了后续的判定方便,MyTask中的时间属性设定为绝对时间。


MyTimer定时器类中定义schedule()方法。其中根据传入的参数构造Task,将其插入队列中即可。注意,schedule()参数中的delay指的是相对时间,如3000,表示的是任务要在3000毫秒后执行。


因此,在构造MyTask时,要有一步绝对时间与相对时间之间的转换:


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


System.currentTimeMillis() 用于获取当前毫秒级别的时间戳,即当前时刻和基准时刻的ms数之差(基准时刻是1970年1月1日00:00:00.000)


代码展现如下:


class MyTask{
    public Runnable runnable;
    public long time;   //为了方便后续判定,使用的是绝对的时间戳
 
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay; //转换:相对时间delay + 当前时间戳 => 绝对的时间戳
    }
}
 
//自定义定时器类
class MyTimer {
    //核心数据结构,带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
 
    //此处的delay是一个相对时间,表示间隔多少时间后执行该任务
    public void schedule(Runnable runnable, long delay) {
        //根据参数构造MyTask,插入队列即可
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
    }
}


b.构造线程来具体执行任务


在计时器类的构造方法中构造扫描线程,用来扫描优先级阻塞队列中的各个任务是否到达可执行的时间。


逻辑很简单:先从queue中取出一个任务,这个任务应是所有任务中时间最小的。再获取当前时间curTime。通过比较curTime与myTask.time的大小来判断是否达到了该任务的执行时间。如果到达了,则执行myTask.runnable.run();如果没有达到,那么只能把刚才取出的任务放回队列中。


//注:该代码不是最终版,仅表明思路
 
class MyTimer {
    ...    //其他代码
 
 
    // 在这里构造线程,负责不停地扫描队首元素,判断该任务是否可以执行
    public MyTimer() {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();  //获取当前时间
                    if(curTime < myTask.time) {
                        //当前时间小于任务时间:时间还没到
                        //暂不执行,要把刚才取出的任务塞回队列中
                        queue.put(myTask);
                    }else{
                        //时间到了,执行任务
                        myTask.runnable.run();  //执行任务
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}


但是上面的代码存在两个严重的问题:


1、当前队列里的MyTask元素是按照什么规则来表示优先级的?

2、while(true)带来了CPU的忙等问题。


Java多线程基础-10:代码案例之定时器(二)+

https://developer.aliyun.com/article/1520554?spm=a2c6h.13148508.setting.17.75194f0e0dvo2R

相关文章
|
2天前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
34 2
|
16天前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
52 5
|
16天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
44 5
|
19天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
15天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
3天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
26 6
|
18天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
16天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
11天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####