Java时间轮算法是一种用于管理和执行定时任务的高效算法·和数据结构。它是一种时间管理T具,常用于需要按照一定的延时或定时要求执行任务的应用场景,如任务调度·、定时器。、超时处理等。该算法使用一个类似于时钟的数据结构,将时间划分为若干个槽(或格子),每个槽表示个时间段,然后将需要执行的任务安排到相应的槽中。
以下是Java时间轮算法的关键特点和组成部分:
- 时间轮数据结构:时间轮通常由多个槽组成,每个槽表示一个时间单位(比如1秒、1毫秒等)这些槽按照时间顺序排列,形成一个环形结构。时间轮可以有不同的粒度,例如,可以有一个秒级时间轮和一个毫秒级时间轮Q,用于处理不同时间范围内的任务。
- 哈希表:每个槽中都有一个哈希表,用于存储在该时间段内需要执行的任务。哈希表的键是任务的触发时间(通常是相对于当前时间的偏移量),值是该时间段内需要执行的任务列表。哈希表的作用是提高任务添加和查找的效率。
- 定时任务:时间轮算法通常包括一个定时任务,该任务以固定的时间间隔轮询时间轮,检查是否2有需要执行的任务。定时任务只是时间轮运行的引擎,通过它,时间轮不断地前进,执行到期的任务
- 任务触发:当时间轮的指针指向某个槽时,定时任务会检查该槽中的哈希表,执行其中所有触发时间到达的任务,并将它们从哈希表中移除。这样,时间轮可以高效地触发任务,而不需要遍历所有任务。
- 任务添加:要添加一个需要在未来某个时间触发的任务,算法会根据任务的触发时间计算出应该放置在哪个槽中的哈希表中。如果触发时间已经过去,任务会立即执行,否则会被安排到相应的槽中等待触发
时间轮算法的具体实现思路如下:
- 创建时间轮数据结构:首先,需要创建一个时间轮数据结构,通常是一个数组,每个元素代表个槽(slot)。这个数组以一定的时间单位划分,例如,可以使用毫秒或秒作为时间单位。数组的大小取决于需要覆盖的时间范围和精度。
- 初始化槽和哈希表:每个槽都包含一个哈希表,用于存储在该槽内需要执行的定时任务。初始化时间轮时,每个槽的哈希表为空。
- 创建定时任务:创建一个定时任务,该任务以一定的时间间隔触发(例如每秒执行一次),用于前进时间轮。这个任务将时间轮的指针不断移动到下一个槽。
- 添加任务:当需要添加一个需要在将来某个时间执行的任务时,计算出任务的触发时间,并将任务添加到相应的槽的哈希表中。如果触发时间已经过去,则立即执行任务。
- 时间轮前进:定时任务的作用是让时间轮不断前进,即指针不断移动到下一个槽。当定时任务触发时,检查当前槽的哈希表,执行其中所有触发时间到达的任务,并从哈希表中移除这些任务。
- 循环运行:时间轮将一直运行,不断前进,检查任务,执行到期的任务,直到没有待执行的任务
Java示例代码,演示了如何实现时间轮算法:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
class TimeWheel {
private int tickMs; // 每一个时间槽的时间间隔
private int wheelSize; // 时间轮的槽数量
private int currentTickIndex = 0; // 当前指针指向的时间槽索引
private List<List<TimerTask>> slots; // 时间槽数组
private ExecutorService executorService; // 线程池,用于执行定时任务
public TimeWheel(int tickMs, int wheelSize) {
this.tickMs = tickMs;
this.wheelSize = wheelSize;
// 初始化时间槽数组
slots = new ArrayList<>(wheelSize);
for (int i = 0; i < wheelSize; i++) {
slots.add(new CopyOnWriteArrayList<>());
}
// 创建线程池
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 启动时间轮指针
start();
}
// 添加定时任务
public void addTimerTask(TimerTask timerTask) {
long expirationMs = timerTask.getExpirationMs();
if (expirationMs < System.currentTimeMillis()) {
return; // 过期任务直接丢弃
}
// 计算定时任务在时间轮中的位置
long delayMs = expirationMs - System.currentTimeMillis();
int ticks = (int) (delayMs / tickMs);
// 将定时任务添加到对应的时间槽中
slots.get((currentTickIndex + ticks) % wheelSize).add(timerTask);
}
// 开始时间轮指针的运转
private void start() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(() -> {
List<TimerTask> currentList = slots.get(currentTickIndex);
// 遍历当前时间槽中的定时任务,并执行
for (TimerTask timerTask : currentList) {
executorService.submit(timerTask::run);
}
// 清空当前时间槽
currentList.clear();
// 指针向下一个时间槽移动
currentTickIndex = (currentTickIndex + 1) % wheelSize;
}, tickMs, tickMs, TimeUnit.MILLISECONDS);
}
}
// 定时任务类
class TimerTask implements Runnable {
private long expirationMs; // 任务的过期时间点
public TimerTask(long delayMs) {
this.expirationMs = System.currentTimeMillis() + delayMs;
}
public long getExpirationMs() {
return expirationMs;
}
@Override
public void run() {
System.out.println("执行定时任务:" + expirationMs);
}
}
public class Main {
public static void main(String[] args) {
TimeWheel timeWheel = new TimeWheel(1000, 10); // 时间间隔为1秒,时间轮大小为10
timeWheel.addTimerTask(new TimerTask(5000)); // 添加一个5秒后触发的定时任务
timeWheel.addTimerTask(new TimerTask(3000)); // 添加一个3秒后触发的定时任务
timeWheel.addTimerTask(new TimerTask(7000)); // 添加一个7秒后触发的定时任务
try {
// 等待定时任务执行完毕
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭时间轮,释放资源
timeWheel.shutdown();
}
}
执行定时任务:3000
执行定时任务:5000
执行定时任务:7000
时间轮算法的主要优缺点如下
优点:
- 调度任务分散,没有集中处理的热点,更易于实现高性能和可扩展性可以平滑任务执行负载,避免任务积压
- 算法本身较为简单易实现
- 可以按需要通过调整时间轮刻度来平衡执行精度和空间消耗
缺点
- 需要占用较大内存空间来存储时间轮数据结构
- 时间轮刻度不能太小,否则造成内存浪费和计算资源消耗需要额外定时线程Q来驱动时间轮走动
- 执行延迟依赖于时间轮刻度,无法做到毫秒级精确
- 实现复杂的任务依赖关系比较困难
本文暂时没有评论,来添加一个吧(●'◡'●)