ScheduledFuture 是 Java 并发编程中用于表示 可调度的异步任务结果 的接口,主要用于 定时/周期性任务 的控制和管理。结合 ConcurrentHashMap 使用可以有效地管理多个定时任务。
1. 继承关系
java
ScheduledFuture<?> extends Delayed, Future<?>
Delayed: 提供 getDelay() 方法,返回任务剩余的延迟时间
Future: 提供任务取消、结果获取等方法
2. 关键方法
java
// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 判断是否已取消
boolean isCancelled();
// 判断是否已完成
boolean isDone();
// 获取结果(对于ScheduledFuture通常返回null)
V get() throws InterruptedException, ExecutionException;
// 获取剩余延迟时间
long getDelay(TimeUnit unit);
典型使用场景
1. 管理定时任务的示例
java
public class TaskScheduler {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
private final Map<Long, ScheduledFuture<?>> runningTasks = new ConcurrentHashMap<>();
// 启动一个定时任务
public void startTask(Long taskId, Runnable task, long initialDelay, long period, TimeUnit unit) {
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
task, initialDelay, period, unit
);
runningTasks.put(taskId, future);
}
// 取消特定任务
public boolean cancelTask(Long taskId) {
ScheduledFuture<?> future = runningTasks.get(taskId);
if (future != null) {
boolean cancelled = future.cancel(true); // true表示中断正在执行的任务
if (canned) {
runningTasks.remove(taskId);
}
return cancelled;
}
return false;
}
// 获取任务状态
public boolean isTaskRunning(Long taskId) {
ScheduledFuture<?> future = runningTasks.get(taskId);
return future != null && !future.isDone() && !future.isCancelled();
}
// 获取所有活动的任务
public List<Long> getActiveTaskIds() {
return runningTasks.entrySet().stream()
.filter(entry -> !entry.getValue().isDone())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
// 优雅关闭
public void shutdown() {
// 取消所有任务
runningTasks.forEach((id, future) -> future.cancel(true));
runningTasks.clear();
scheduler.shutdown();
}
}
2. 任务执行模式
java
// 1. 延迟执行(一次)
ScheduledFuture<?> future = scheduler.schedule(
task, 5, TimeUnit.SECONDS
);
// 2. 固定速率执行(忽略任务执行时间)
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
task, 0, 1, TimeUnit.SECONDS // 立即开始,每秒执行一次
);
// 3. 固定延迟执行(等待任务完成后再计算延迟)
ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(
task, 0, 1, TimeUnit.SECONDS // 任务完成后等待1秒再执行下一次
);
最佳实践建议
1. 异常处理
java
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
try {
// 业务逻辑
} catch (Exception e) {
// 必须捕获异常,否则定时任务会停止
log.error("Task execution failed", e);
}
}, 0, 1, TimeUnit.SECONDS);
2. 内存泄漏防范
java
// 定期清理已完成的任务
public void cleanupCompletedTasks() {
Iterator<Map.Entry<Long, ScheduledFuture<?>>> iterator = runningTasks.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, ScheduledFuture<?>> entry = iterator.next();
if (entry.getValue().isDone()) {
iterator.remove();
}
}
}
3. 监控任务执行
java
// 监控任务执行状态
public void monitorTasks() {
runningTasks.forEach((taskId, future) -> {
long delay = future.getDelay(TimeUnit.MILLISECONDS);
boolean cancelled = future.isCancelled();
boolean done = future.isDone();
System.out.printf("Task %d: delay=%dms, cancelled=%s, done=%s%n",
taskId, delay, cancelled, done);
});
}
注意事项:
1.资源管理: 确保在应用关闭时取消所有任务,避免线程泄漏
2.任务标识: 使用合适的键类型(如任务ID、UUID等)来标识任务
3.并发安全: ConcurrentHashMap 提供线程安全,但操作组合可能仍需同步
4.任务隔离: 避免任务间相互影响,特别是异常传播
5.性能考虑: 大量任务时考虑使用 ScheduledThreadPoolExecutor 的调优参数
项目应用实践:
项目需求介绍:实现两种方式的定时调用功能:

具体代码实现:
java
package com.mxpt.resource.manage.service.impl;
import com.mxpt.common.core.constant.SecurityConstants;
import com.mxpt.resource.manage.domain.*;
import com.mxpt.resource.manage.service.IResourceCalcSceneRollSettingService;
import com.mxpt.system.api.RemoteCustomConfigService;
import com.mxpt.system.api.domain.SysCustomConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
public class RollingForecastSchedulerService {
private static final Logger logger = LoggerFactory.getLogger(RollingForecastSchedulerService.class);
@Autowired
private IResourceCalcSceneRollSettingService resourceCalcSceneRollSettingService;
@Autowired
private RemoteCustomConfigService remoteCustomConfigService;
// 任务调度器
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
// 存储正在运行的任务
private final Map<Long, ScheduledFuture<?>> runningTasks = new ConcurrentHashMap<>();
// 线程工厂,用于创建命名线程
private final ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "RollForecast-Task-" + threadNumber.getAndIncrement());
thread.setDaemon(true);
return thread;
}
};
/**
* 初始化时加载所有启用的定时任务
*/
@PostConstruct
public void init() {
try {
logger.info("开始初始化滚动预报定时任务...");
loadAndScheduleAllTasks();
logger.info("滚动预报定时任务初始化完成,当前运行任务数:{}", runningTasks.size());
} catch (Exception e) {
logger.error("初始化滚动预报定时任务失败", e);
}
}
/**
* 加载并调度所有启用的任务
*/
private void loadAndScheduleAllTasks() {
try {
// 查询所有启用的滚动预报配置
ResourceCalcSceneRollSetting obj = new ResourceCalcSceneRollSetting();
obj.setStatus(1); // 1表示启用状态
obj.setIsRollHistory(0); // 0表示非历史滚动任务
List<ResourceCalcSceneRollSetting> enabledSettings = resourceCalcSceneRollSettingService.selectResourceCalcSceneRollSettingList(obj);
if(enabledSettings == null || enabledSettings.size() == 0){
logger.info("没有启用的滚动预报任务需要调度");
return;
}
for (ResourceCalcSceneRollSetting setting : enabledSettings) {
try {
scheduleTask(setting);
} catch (Exception e) {
logger.error("调度任务失败,任务ID: {}", setting.getRollId(), e);
resourceCalcSceneRollSettingService.logJobExecution(setting, "任务调度失败: " + e.getMessage(), "1", null, "加载并调度所有启用的任务", "加载并调度所有启用的任务:loadAndScheduleAllTasks()", e.getMessage());
}
}
} catch (Exception e) {
logger.error("加载定时任务配置失败", e);
}
}
/**
* 调度单个任务
*/
public void scheduleTask(ResourceCalcSceneRollSetting setting) {
Long rollId = setting.getRollId();
// 如果任务已经在运行,先取消
if (runningTasks.containsKey(rollId)) {
cancelTask(rollId);
}
// 根据任务类型创建不同的调度策略
Runnable task = createTask(setting);
ScheduledFuture<?> future = null;
if (setting.getRollType().equals(RollJobType.FIXED_TIME.getCode())) {
// 固定时间点触发
future = scheduleFixedTimeTask(task, setting);
} else if (setting.getRollType().equals(RollJobType.INTERVAL_TIME.getCode())) {
// 间隔时间触发
future = scheduleIntervalTask(task, setting);
}
if (future != null) {
runningTasks.put(rollId, future);
logger.info("任务调度成功,任务ID: {}, 任务名称: {}", rollId, setting.getRollName());
}
}
/**
* 创建任务Runnable
*/
private Runnable createTask(ResourceCalcSceneRollSetting setting) {
return () -> {
Long rollId = setting.getRollId();
String taskName = setting.getRollName();
logger.info("开始执行滚动预报任务,任务ID: {}, 任务名称: {}", rollId, taskName);
try {
// 1. 调用预报接口
Map<String, Object> jobLogs = callForecastApi(setting);
// 2. 记录成功日志
Long calcsceneId = Long.valueOf(jobLogs.get("calcsceneId").toString());
String rollJobName = jobLogs.get("rollJobName").toString();
String rollInvokeTarget = jobLogs.get("rollInvokeTarget").toString();
resourceCalcSceneRollSettingService.logJobExecution(setting, "任务执行成功", "0", calcsceneId, rollJobName, rollInvokeTarget, null);
logger.info("滚动预报任务执行完成,任务ID: {}, 生成的场次ID: {}", rollId, calcsceneId);
} catch (Exception e) {
logger.error("执行滚动预报任务失败,任务ID: {}", rollId, e);
String rollInvokeTarget = "调用接口:/resource-manage/resourceCalc/addResourceCalcScene,当前场次名称已经存在!" ;
// 计算预报时间
Date forecastTime = calculateForecastTime(setting);
if(forecastTime == null) {
throw new RuntimeException("当前未到预报时间,无法执行滚动预报任务");
}
// 生成计算场景名称
String rollJobName = resourceCalcSceneRollSettingService.generateCalcSceneName(setting, forecastTime);
resourceCalcSceneRollSettingService.logJobExecution(setting, "任务执行失败: " + e.getMessage(), "1", null, rollJobName, rollInvokeTarget, e.getMessage());
}
};
}
/**
* 调用预报接口
*/
private Map callForecastApi(ResourceCalcSceneRollSetting setting) {
try {
// 计算预报时间
Date forecastTime = calculateForecastTime(setting);
if(forecastTime == null) {
throw new RuntimeException("当前未到预报时间,无法执行滚动预报任务");
}
//对函数进行封装用于其他地方调用
return resourceCalcSceneRollSettingService.callForecastApiPlus(setting, forecastTime);
} catch (Exception e) {
throw new RuntimeException("调用预报接口失败: " + e.getMessage(), e);
}
}
/**
* 计算预报时间
* 根据滚动方案类型和配置计算本次执行的预报时间
*/
private Date calculateForecastTime(ResourceCalcSceneRollSetting setting) {
Date now = new Date();
if (setting.getRollType() == 1) {
// 固定时间点触发 - 使用配置的时间点作为预报时间
try {
// 解析配置的时间,如"20:00"
String timeStr = setting.getRollInterval();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
Date configuredTime = sdf.parse(timeStr);
// 设置到今天
SimpleDateFormat dateSdf = new SimpleDateFormat("yyyy-MM-dd");
String todayStr = dateSdf.format(now) + " " + timeStr + ":00";
SimpleDateFormat fullSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return fullSdf.parse(todayStr);
} catch (Exception e) {
// 解析失败,返回当前时间
return null;
}
} else if (setting.getRollType() == 2) {
// 间隔时间触发 - 例如每60分钟触发一次,预报时间为setting.getRollStartTime() + n * interval
try {
// 获取间隔时间(分钟)
int intervalMinutes = Integer.parseInt(setting.getRollInterval());
long intervalMillis = intervalMinutes * 60 * 1000L;
// 计算从开始时间到现在的间隔次数
long elapsedMillis = now.getTime() - setting.getRollStartTime().getTime();
long intervalsPassed = elapsedMillis / intervalMillis;
// 计算本次预报时间
long forecastTimeMillis = setting.getRollStartTime().getTime() + intervalsPassed * intervalMillis;
return new Date(forecastTimeMillis);
} catch (Exception e) {
// 解析失败,返回当前时间
return null;
}
}
return null;
}
/**
* 调度固定时间点任务(支持延迟启动)
*/
private ScheduledFuture<?> scheduleFixedTimeTask(Runnable task, ResourceCalcSceneRollSetting setting) {
String timeStr = setting.getRollInterval(); // 格式如 "20:00"
try {
// 解析目标时间
LocalTime targetTime = LocalTime.parse(timeStr, DateTimeFormatter.ofPattern("HH:mm"));
LocalDateTime now = LocalDateTime.now();
// 获取滚动预报延迟时间(单位:分钟)
long rollDelayMinutes = 10L; // 默认10分钟
Long deptId = setting.getDeptId();
if (deptId != null && !deptId.equals(0L)) {
SysCustomConfig rollDelayMinutesConfig = remoteCustomConfigService.selectSysCustomConfigByKeyDeptId(
"roll_delay_minutes", deptId, SecurityConstants.INNER);
if (rollDelayMinutesConfig != null && rollDelayMinutesConfig.getConfigValue() != null) {
try {
rollDelayMinutes = Long.parseLong(rollDelayMinutesConfig.getConfigValue());
} catch (NumberFormatException e) {
logger.warn("解析延迟时间配置失败,使用默认值10分钟: {}", e.getMessage());
}
}
}
// 计算第一次执行时间:目标时间 + 延迟分钟数
LocalDateTime nextRunTime = now.with(targetTime).plusMinutes(rollDelayMinutes);
// 如果今天的时间已经过了,就安排到明天
if (now.isAfter(nextRunTime)) {
nextRunTime = nextRunTime.plusDays(1);
}
// 计算初始延迟(毫秒)
long initialDelay = java.time.Duration.between(now, nextRunTime).toMillis();
// 记录日志
logger.info("固定时间点任务调度信息: 目标时间={}, 延迟时间={}分钟, 实际执行时间={}, 当前时间={}, 初始延迟={}ms",
targetTime, rollDelayMinutes, nextRunTime, now, initialDelay);
// 安排任务,每天执行一次(24小时)
return scheduler.scheduleAtFixedRate(
task,
initialDelay,
24 * 60 * 60 * 1000L, // 24小时
TimeUnit.MILLISECONDS
);
} catch (Exception e) {
throw new RuntimeException("解析时间格式错误: " + timeStr, e);
}
}
/**
* 调度间隔时间任务(固定延迟版本)
*/
private ScheduledFuture<?> scheduleIntervalTask(Runnable task, ResourceCalcSceneRollSetting setting) {
String intervalStr = setting.getRollInterval(); // 格式如 "60"(分钟)
try {
int intervalMinutes = Integer.parseInt(intervalStr);
long intervalMillis = intervalMinutes * 60 * 1000L;
// 计算开始时间
LocalDateTime startTime = setting.getRollStartTime().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
LocalDateTime now = LocalDateTime.now();
// 获取滚动预报延迟时间(单位:分钟)
long rollDelayMinutes = 10L; // 默认10分钟
Long deptId = setting.getDeptId();
if (deptId != null && !deptId.equals(0L)) {
SysCustomConfig rollDelayMinutesConfig = remoteCustomConfigService.selectSysCustomConfigByKeyDeptId(
"roll_delay_minutes", deptId, SecurityConstants.INNER);
if (rollDelayMinutesConfig != null && rollDelayMinutesConfig.getConfigValue() != null) {
try {
rollDelayMinutes = Long.parseLong(rollDelayMinutesConfig.getConfigValue());
} catch (NumberFormatException e) {
// 使用默认值
}
}
}
// 计算第一次执行时间:开始时间 + 延迟分钟数
LocalDateTime firstExecutionTime = startTime.plusMinutes(rollDelayMinutes);
// 计算初始延迟
long initialDelay;
if (now.isBefore(firstExecutionTime)) {
// 还没到第一次执行时间,计算延迟
initialDelay = java.time.Duration.between(now, firstExecutionTime).toMillis();
} else {
// 已经过了第一次执行时间,需要计算下一次执行时间
// 计算从开始时间到现在经过了多少个间隔
long minutesSinceStart = java.time.Duration.between(startTime, now).toMinutes();
// 计算已经执行了多少次(考虑延迟)
long executionsSinceStart = (minutesSinceStart - rollDelayMinutes) / intervalMinutes;
// 计算下一次执行时间(上一次执行时间 + 间隔)
LocalDateTime nextExecutionTime = firstExecutionTime
.plusMinutes(executionsSinceStart * intervalMinutes)
.plusMinutes(intervalMinutes);
initialDelay = java.time.Duration.between(now, nextExecutionTime).toMillis();
// 如果已经过了下一次执行时间(理论上不应该发生,但安全处理)
if (initialDelay < 0) {
initialDelay = 0;
}
}
// 记录日志(用于调试)
logger.info("任务调度信息: 开始时间={}, 第一次执行时间={}, 当前时间={}, 初始延迟={}ms, 间隔={}ms",
startTime, firstExecutionTime, now, initialDelay, intervalMillis);
// 使用scheduleWithFixedDelay实现固定延迟调度
return scheduler.scheduleWithFixedDelay(
task,
initialDelay,
intervalMillis,
TimeUnit.MILLISECONDS
);
} catch (NumberFormatException e) {
throw new RuntimeException("解析间隔时间错误: " + intervalStr, e);
}
}
/**
* 取消任务
*/
public void cancelTask(Long rollId) {
ScheduledFuture<?> future = runningTasks.get(rollId);
if (future != null) {
future.cancel(true);
runningTasks.remove(rollId);
logger.info("已取消任务,任务ID: {}", rollId);
}
}
/**
* 重新加载所有任务(可用于配置更新后调用)
*/
public void reloadAllTasks() {
try {
logger.info("重新加载所有定时任务...");
// 取消所有当前任务
for (Long rollId : runningTasks.keySet()) {
cancelTask(rollId);
}
// 重新加载和调度
loadAndScheduleAllTasks();
logger.info("定时任务重新加载完成,当前运行任务数: {}", runningTasks.size());
} catch (Exception e) {
logger.error("重新加载定时任务失败", e);
}
}
/**
* 获取所有运行中的任务
*/
public Map<Long, String> getRunningTasks() {
Map<Long, String> tasks = new HashMap<>();
runningTasks.forEach((rollId, future) -> {
String status = future.isCancelled() ? "已取消" :
future.isDone() ? "已完成" : "运行中";
tasks.put(rollId, status);
});
return tasks;
}
/**
* 立即执行指定任务(手动触发)
*/
public void executeTaskImmediately(Long rollId) {
try {
ResourceCalcSceneRollSetting setting = resourceCalcSceneRollSettingService.selectResourceCalcSceneRollSettingByRollId(rollId);
if (setting == null) {
throw new RuntimeException("任务不存在,ID: " + rollId);
}
if (!setting.getStatus().equals(1)) {
throw new RuntimeException("任务未启用,ID: " + rollId);
}
Runnable task = createTask(setting);
CompletableFuture.runAsync(task);
logger.info("已触发立即执行任务,任务ID: {}", rollId);
} catch (Exception e) {
logger.error("触发立即执行任务失败", e);
throw new RuntimeException("触发任务失败: " + e.getMessage(), e);
}
}
/**
* 定时检查任务状态(每5分钟执行一次)
*/
@Scheduled(fixedRate = 5 * 60 * 1000)
public void checkTaskStatus() {
try {
logger.debug("开始检查定时任务状态...");
// 检查数据库中是否有新的启用任务
// 查询所有启用的滚动预报配置
ResourceCalcSceneRollSetting obj = new ResourceCalcSceneRollSetting();
obj.setStatus(1); // 1表示启用状态
obj.setIsRollHistory(0); // 0表示非历史滚动任务
List<ResourceCalcSceneRollSetting> currentEnabledSettings = resourceCalcSceneRollSettingService.selectResourceCalcSceneRollSettingList(obj);
if(currentEnabledSettings == null || currentEnabledSettings.size() == 0){
logger.info("没有启用的滚动预报任务");
return;
}
// 检查是否有任务需要启动或停止
for (ResourceCalcSceneRollSetting setting : currentEnabledSettings) {
if (!runningTasks.containsKey(setting.getRollId())) {
// 数据库启用但调度器中没有,需要启动
logger.info("发现新启用任务,开始调度,任务ID: {}", setting.getRollId());
scheduleTask(setting);
}
}
// 检查是否有任务需要停止
// 使用Java 8的Collectors.toList()
List<Long> tasksToRemove = runningTasks.keySet().stream()
.filter(rollId -> currentEnabledSettings.stream()
.noneMatch(setting -> setting.getRollId().equals(rollId)))
.collect(Collectors.toList());
for (Long rollId : tasksToRemove) {
logger.info("发现已禁用任务,停止调度,任务ID: {}", rollId);
cancelTask(rollId);
}
} catch (Exception e) {
logger.error("检查定时任务状态失败", e);
}
}
/**
* 应用关闭时清理资源
*/
public void shutdown() {
try {
logger.info("开始关闭定时任务调度器...");
// 取消所有任务
for (Long rollId : runningTasks.keySet()) {
cancelTask(rollId);
}
// 关闭调度器
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("定时任务调度器已关闭");
} catch (Exception e) {
logger.error("关闭定时任务调度器失败", e);
}
}
}