Java 并发编程-ScheduledFuture

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);
        }
    }
}
相关推荐
我是Superman丶8 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
墨雨晨曦888 小时前
Nacos
java
__如风__8 小时前
onlyoffice文档转换服务离线部署
python
invicinble8 小时前
seata的认识与实际开发要做的事情
java
今晚务必早点睡8 小时前
写一个Python接口:发送支付成功短信
开发语言·python
weixin_584121438 小时前
vue内i18n国际化移动端引入及使用
前端·javascript·vue.js
ada7_8 小时前
LeetCode(python)22.括号生成
开发语言·数据结构·python·算法·leetcode·职场和发展
2501_941871458 小时前
面向微服务链路追踪与全局上下文管理的互联网系统可观测性设计与多语言工程实践分享
大数据·数据库·python
luoluoal8 小时前
基于python的语音和背景音乐分离算法及系统(源码+文档)
python·mysql·django·毕业设计·源码
Delroy8 小时前
一个不懂MCP的开发使用vibe coding开发一个MCP
前端·后端·vibecoding