一.定时器
1.什么是定时器
在Java中定时器通常指的是一种能够按照预定的时间间隔执行任务的机制,简单来说,定时器就相当于一个"闹钟",指定一个(Runnable) 任务,以及指定一个时间,该任务因为定时器的缘故在线程中并不会立马就执行,而是到达某个指定的时间后,才执行
2.定时器的应用场景
Java中的定时器(Timer)在许多应用场景中都非常有用,下面是几个常见的例子:
- 定时任务执行:
- 定期清理缓存:应用程序可以设置定时器来定时清理不再需要的缓存数据,避免内存占用过高。
- 数据备份:数据库系统可以使用定时器在每天的固定时间执行数据备份操作。
- 日志滚动:系统可以按计划滚动日志文件,比如每24小时创建一个新的日志文件。
- 周期性更新:
- 动态刷新数据:网页应用程序中,定时器可用于刷新显示的内容,例如实时股票价格、天气预报等。
- API轮询:在等待异步响应或资源的状态变化时,可以设置定时器定期发起API请求查询最新状态。
- 邮件通知:
- 提醒服务:例如,用户注册后发送确认邮件,或者每日/每周发送新闻简报、报告等。
- 系统监控告警:当系统指标超过阈值时,定时器可以用来检查系统状态并在必要时发送警告邮件。
- 会话管理:
- 用户会话过期处理:在Web应用中,定时器可以用来检查并清理长时间未活动的用户会话。
- 心跳检测:
- 网络连接健康检查:客户端和服务端之间可以利用定时器发送心跳包来维持长连接,及时发现连接异常。
- 延迟执行:
- 延迟发送消息:例如在消息队列中,可以设置一条消息在一段时间后才投递给消费者。
- 调度任务:
- 批处理作业:在企业级应用中,定时器可以调度批处理作业在非高峰时段执行,减轻服务器负载。
- 社交媒体更新:
- 社交媒体平台可以使用定时器定期检查用户的动态更新,例如好友状态更改或新消息提醒。
总之,任何需要按照预设时间间隔或特定时间点执行任务的地方都可以考虑使用定时器机制。
3.如何在Java中创建定时器
在Java中创建定时器通常有两种方式,使用java.util.Timer
类或ScheduledExecutorService
接口。以下是两种方式的基本示例:
1.使用 Java.util.Timer 类
import java.util.Timer; import java.util.TimerTask; public class TimerExample { public static void main(String[] args) { Timer timer = new Timer(); // 任务类 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("任务执行了"); } }; // 安排任务在1秒后执行,然后每隔1秒执行一次 timer.scheduleAtFixedRate(task, 1000, 1000); // 记得在程序结束时取消定时器,以避免潜在的内存泄漏 // timer.cancel(); } }
2.使用ScheduledExecutorService
接口
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ScheduledExecutorExample { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); Runnable task = new Runnable() { @Override public void run() { System.out.println("Scheduled任务执行了"); } }; // 安排任务在1秒后执行,然后每隔1秒执行一次 executor.scheduleAtFixedRate(task, 1, 1, TimeUnit.SECONDS); // 记得在程序结束时关闭executor,以释放资源 // executor.shutdown(); } }
3.两个方法的注意事项
- 使用
Timer
时,所有的任务都由同一个线程顺序执行,如果某个任务执行时间过长,可能会影响其他任务的执行时间。 - 使用
ScheduledExecutorService
时,可以控制任务执行的线程池,这使得它更适合执行可能阻塞的任务,或者需要并行执行的任务。 - 在应用程序不再需要定时器时,应该调用
timer.cancel()
或executor.shutdown()
来停止定时器,避免线程泄露。 - 如果希望立即停止所有正在执行的任务,并不再接受新的任务,可以使用
executor.shutdownNow()
方法 - 简单来说,使用 Java.util.Timer 类 就是在后台创建一个 线程或多个线程 默认情况下,
Timer
只创建一个线程来顺序执行所有任务。这意味着如果一个任务的执行时间过长,它将阻塞队列中的其他任务。线程池允许更好地控制任务的执行,例如设置最大线程数、线程存活时间、工作队列等。使用线程池可以有效地管理资源,提高程序的响应速度和效率。
4.定时器的底层执行顺序
Java java.util.Timer
类作为基础定时器实现,其底层执行顺序遵循以下逻辑:
- 任务调度:
- 当用户创建
Timer
实例并使用schedule()
或scheduleAtFixedRate()
方法来调度TimerTask
时,这些任务会被添加到Timer
的内部TaskQueue
队列中。这个队列是按照任务的执行时间进行排序的,即按照每个任务下一次应该被执行的时间。
- 任务轮询:
Timer
创建一个名为TimerThread
的后台线程,这个线程会在启动后进入一个无限循环。- 在循环中,
TimerThread
不断地检查TaskQueue
中是否有已到达执行时间的任务。 - 如果发现一个或多个任务已经到期,则按照队列的顺序取出任务并执行它的
run()
方法。
- 执行顺序保证:
- 根据时间优先级排序,最先到期的任务会先被执行。
- 对于固定延迟调度(
schedule()
),任务的实际执行时间可能受前一个任务执行时间和线程调度的影响,不一定严格按计划时间执行。 - 对于固定速率调度(
scheduleAtFixedRate()
),即使前一个任务执行超时导致延迟,系统仍尝试按照固定的频率执行任务,即从上一次任务开始执行的时间点算起,每隔一定时间执行一次。
- 并发问题:
- 由于
Timer
是基于单线程模型的,如果一个任务执行时间过长或者抛出未捕获的异常,那么后续的任务可能会延迟执行,甚至无法执行。这是因为单个Timer
的所有任务都在同一个TimerThread
上执行,这意味着任务之间没有并发执行能力,且一旦遇到阻塞或异常情况,整个定时器的工作将会受到影响。
因此,虽然理论上 Timer
底层执行顺序是严格按照任务设定的时间顺序进行的,但在实际应用中,尤其是考虑到多任务环境下的线程调度不确定性以及单线程模型的局限性,应当谨慎使用 java.util.Timer
类以避免潜在的并发问题。对于更复杂或对定时任务执行顺序和可靠性要求较高的场景,推荐使用诸如 ScheduledExecutorService
这样的并发框架提供的服务。
5.使用java代码自定义一个定时器
通过了解定时器的底层执行顺序,为了帮助我们更好的理解定时器,我们就可以自己定义一个定时器.增加理解
// 定义一个实现了Comparable接口的定时任务类,用于存储Runnable任务及其执行时间 class MyTimerTask implements Comparable<MyTimerTask> { private Runnable runnable; // Runnable对象,代表具体要执行的任务 private long time; // 记录该任务应该被执行的具体时间(系统当前时间+延迟时间) // 构造方法,传入一个Runnable任务和延迟执行的时间 public MyTimerTask(Runnable runnable, long delay) { this.runnable = runnable; this.time = System.currentTimeMillis() + delay; } // 获取任务的执行时间 public long getTime() { return time; } // 执行任务 public void run() { runnable.run(); } // 重写compareTo方法,用于比较两个任务的执行时间,以便放入PriorityQueue按时间顺序排序 @Override public int compareTo(MyTimerTask o) { return (int) (this.getTime() - o.getTime()); } } // 定时器类,负责调度任务 class MyTimer { private PriorityQueue<MyTimerTask> q = new PriorityQueue<>(); // 存储定时任务的优先级队列 private Object locker = new Object(); // 同步锁对象,用于线程同步 // 定时器构造方法,启动一个后台线程执行定时任务 public MyTimer() { Thread t = new Thread(() -> { try { // 无限循环,直到程序结束 while (true) { synchronized (locker) { // 如果队列为空,则等待新的任务加入 if (q.size() == 0) { locker.wait(); } // 获取队列中下一个即将执行的任务 MyTimerTask myTimerTask = q.peek(); long nowTime = System.currentTimeMillis(); // 检查当前时间是否大于等于任务执行时间 if (nowTime >= myTimerTask.getTime()) { // 执行任务并从队列中移除已完成的任务 myTimerTask.run(); q.poll(); } else { // 如果任务未到执行时间,线程等待剩余时间 locker.wait(myTimerTask.getTime() - nowTime); } } } } catch (InterruptedException e) { throw new RuntimeException(e); } }); t.start(); } // 调度方法,将一个任务及其延迟执行时间添加到定时器中 public void schedule(Runnable runnable, long delay) { synchronized (locker) { MyTimerTask task = new MyTimerTask(runnable, delay); // 创建一个新的定时任务 q.offer(task); // 将任务添加到优先级队列中 locker.notify(); // 唤醒等待的任务调度线程 } } } // 主函数,演示如何使用MyTimer类调度任务 public class Demo18 { public static void main(String[] args) { MyTimer myTimer = new MyTimer(); // 调度三个任务,分别延迟1000ms、2000ms和3000ms执行 myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 1000"); } }, 1000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 2000"); } }, 2000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello 3000"); } }, 3000); } }
1.代码解析
MyTimerTask
类是一个自定义的定时任务类,它封装了一个Runnable
对象(即任务实体)和一个表示任务执行时间的long
变量。由于实现了Comparable
接口,任务可以根据其执行时间在优先级队列中进行排序。MyTimer
类是定时器类,维护了一个优先级队列 (PriorityQueue
) 来存放MyTimerTask
对象。在构造方法中启动了一个后台线程,该线程会不断地检查并执行优先级队列中最接近当前时间的任务。- 在后台线程的无限循环中,首先检查优先级队列是否为空。如果为空,则调用
wait()
方法让当前线程进入阻塞状态,直到有新的任务被添加到队列中。然后取出队列顶端的任务,判断其执行时间是否已到。如果已到则执行任务并移除,否则继续等待剩余时间。 schedule
方法允许外部向定时器中添加一个Runnable
任务和对应的延迟执行时间。将任务封装成MyTimerTask
并添加至优先级队列中,随后调用locker.notify()
唤醒等待的任务调度线程,使其重新检查队列并执行任务。
2.注意事项
1.该代码只是帮助更好的理解定时器的执行顺序以及一些执行原理,如果我们实际开发的时候需要用到定时器的话,我们最好的使用 java.util.Timer
类或ScheduledExecutorService
接口来定义定时器,
感谢你的阅读,祝你一天愉快