- 👑专栏内容:Java
- ⛪个人主页:子夜的星的主页
- 💕座右铭:前路未远,步履不停
目录
- 一、`Timer`类
- 二、手动实现定时器
- 1、实现逻辑
- 2、问题描述
- 2.1、问题一:线程安全问题
- 2.2、问题二:使用 `sleep` 休眠
- 2.3、问题三:没有比较规则
- 3、代码实现
一、Timer
类
java.util.Timer
类是 Java 中内置的定时器类,用于在指定的时间或时间间隔执行任务。
方法名 | 参数 | 描述 |
---|---|---|
Timer() | 无 | 创建一个计时器并启动该计时器 |
schedule(TimerTask task, long delay) | task: 要执行的任务, delay: 延迟时间(毫秒) | 安排在指定延迟后执行一次任务 |
schedule(TimerTask task, long delay, long period) | task: 要执行的任务, delay: 延迟时间(毫秒), period: 重复执行的时间间隔(毫秒) | 安排在指定延迟后开始重复执行任务,每次执行之间间隔指定的时间 period |
scheduleAtFixedRate(TimerTask task, long delay, long period) | task: 要执行的任务, delay: 延迟时间(毫秒), period: 重复执行的时间间隔(毫秒) | 安排在指定延迟后开始重复执行任务,每次执行之间间隔固定的时间 period |
cancel() | 无 | 中止该计时器,并放弃所有已安排的任务,对当前正在执行的任务没有影响 |
purge() | 无 | 将所有已取消的任务移除,一般用来释放内存空间 |
定时器示例:
import java.util.Timer;
import java.util.TimerTask;
// 定时器的使用
public class Demo {
public static void main(String[] args) {
Timer timer = new Timer();
//给 timer中注册的这个任务,不上在调用schedule的线程中执行的,而是通过Timer内部的线程来负责执行的。
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务!");
}
},3000);
System.out.println("程序开始运行!");
}
}
import java.util.Timer;
import java.util.TimerTask;
// 定时器的使用
public class Demo {
public static void main(String[] args) {
Timer timer = new Timer();
//给 timer中注册的这个任务,不上在调用schedule的线程中执行的,而是通过Timer内部的线程来负责执行的。
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务 1!");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务 2!");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务 3!");
}
},3000);
System.out.println("程序开始运行!");
}
}
注意事项:
Timer
类是非线程安全的,因此在多线程环境下使用时需要注意同步问题。Timer
类在执行任务时可能会抛出异常,因此需要在任务中进行异常处理。Timer
类在JVM退出时不会自动销毁,因此需要在程序退出时显式销毁计时器。
二、手动实现定时器
1、实现逻辑
我们手动创建一个 TimerTask
类来描述一个定时任务,该类包含以下两个方面:
- 任务: 要执行的任务代码,可以是任何
Runnable
实例。 - 实际执行时间: 使用时间戳表示任务的执行时间。
数据结构选择:
为了高效地管理多个定时任务,我们需要选择合适的数据结构来组织它们。使用普通List(数组或链表)会存在以下问题:
- 无法快速确定何时执行特定任务,需要遍历整个List进行比较。
- 当任务数量巨大时,遍历操作会消耗大量时间,降低效率。
优先级队列:
使用优先级队列可以有效解决上述问题。优先级队列是一种根据元素的优先级排序的数据结构,队首元素始终是优先级最高的元素。
实现原理:
- 将所有任务按照实际执行时间排序,并插入到优先级队列中。
- 创建一个守护线程不断扫描队列:
- 如果队首元素的实际执行时间未到,则等待一段时间再进行下一次扫描。
- 如果队首元素的实际执行时间已到,则取出该元素并执行其任务。
优势:
- 能够快速确定何时执行特定任务,只需检查队首元素即可。
- 即使任务数量巨大,也能保持高效的执行效率,因为只需要关注队首元素。
// 描述定时器的一个任务
class MyTimeTask{
//啥时候执行,毫秒时间戳
private long time;
//任务是什么
private Runnable runnable;
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
public MyTimeTask(Runnable runnable, long delay){
//delay 是一个相对时间差,形如3000这种
//构造time要根据当前系统的时间和dalay构造
time = System.currentTimeMillis() + delay;
this.runnable = runnable;
}
}
class Mytimer{
//优先级队列保存上述的N个任务
private PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
//定时器的核心方法:要执行的任务添加到队列中
private void schedule(Runnable runnable,long delay){
MyTimeTask task = new MyTimeTask(runnable,delay);
queue.offer(task);
}
//MyTimer 中还需要构造一个扫描线程,一方面负责监控队首元素是否执行,一方面当任务到点后
// 调用这个Runnable的run方法完成任务
private Mytimer(){
//扫描线程
Thread t = new Thread(()->{
while(true){
try {
if(queue.isEmpty()){
//当前队列为空,不应该去取元素,直接跳过
continue;
}
MyTimeTask task = queue.peek();
long curTime = System.currentTimeMillis();
if(curTime>= task.getTime()){
//假设当前时间是 14.01,任务时间是 14.00,就是需要执行任务
queue.poll();
task.getRunnable().run();
}else{
//让当前扫描线程休眠,按照时间差进行休眠。
Thread.sleep(task.getTime()-curTime);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
2、问题描述
2.1、问题一:线程安全问题
PriorityQueue
是非线程安全的,在多线程环境下使用可能会导致数据错乱。
解决方案:
- 使用
concurrent
包中的PriorityQueue
实现。 - 使用 synchronized 关键字对
PriorityQueue
进行加锁。
2.2、问题二:使用 sleep
休眠
sleep
方法会阻塞当前线程,导致其他线程无法执行。
解决方案:
- 使用
wait
方法代替sleep
方法,使线程进入等待状态,并释放 CPU 资源。 - 使用
notify
或notifyAll
方法唤醒等待线程。
2.3、问题三:没有比较规则
PriorityQueue
要求放入其中的元素必须实现 Comparable
接口或提供 Comparator
比较器,以便根据元素的优先级进行排序。
解决方案:
- 在
TimerTask
类中实现Comparable
接口,重写compareTo
方法定义比较规则。 - 在创建
PriorityQueue
时传入自定义的Comparator
比较器。
3、代码实现
package thread;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;
// 描述定时器的一个任务
class MyTimeTask implements Comparable<MyTimeTask>{
//啥时候执行,毫秒时间戳
private long time;
//任务是什么
private Runnable runnable;
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimeTask o) {
// 认为时间小的优先级高,时间最小的放到队首。
return (int)(this.time - o.time);
}
public MyTimeTask(Runnable runnable, long delay){
//delay 是一个相对时间差,形如3000这种
//构造time要根据当前系统的时间和dalay构造
time = System.currentTimeMillis() + delay;
this.runnable = runnable;
}
}
class Mytimer{
private Object locker = new Object();
//优先级队列保存上述的N个任务
private PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();
//定时器的核心方法:要执行的任务添加到队列中
void schedule(Runnable runnable, long delay){
synchronized (locker){
MyTimeTask task = new MyTimeTask(runnable,delay);
queue.offer(task);
//每次来新的任务,都唤醒一下扫描线程。
locker.notify();
}
}
//MyTimer 中还需要构造一个扫描线程,一方面负责监控队首元素是否执行,一方面当任务到点后
// 调用这个Runnable的run方法完成任务
public Mytimer(){
//扫描线程
Thread t = new Thread(()->{
while(true){
try {
synchronized (locker){
while(queue.isEmpty()){
//当前队列为空,不应该去取元素,直接跳过
//此处使用wait等待更合适,使用continue就会让while循环运行飞快,陷入高频占用CPU的状态(盲等)。
locker.wait();
}
MyTimeTask task = queue.peek();
long curTime = System.currentTimeMillis();
if(curTime>= task.getTime()){
//假设当前时间是 14.01,任务时间是 14.00,就是需要执行任务
queue.poll();
task.getRunnable().run();
}else{
//让当前扫描线程休眠,按照时间差进行休眠。
//Thread.sleep(task.getTime()-curTime);
locker.wait(task.getTime()-curTime);
}
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo {
// 一个定时器
public static void main(String[] args) {
Mytimer timer = new Mytimer();
//给 timer中注册的这个任务,不上在调用schedule的线程中执行的,而是通过Timer内部的线程来负责执行的。
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行定时任务 1!");
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行定时任务 2!");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行定时任务 3!");
}
},3000);
System.out.println("程序开始运行!");
}
}