ScheduledThreadPoolExecutor使用

0 阅读3分钟

简介

最近遇到了一个业务场景,系统有个功能可以新增服务监测,设置采集周期(/秒),例如每 5 秒、10 秒 采集一次

代码中需要根据设置的周期去采集服务器的状态数据,将状态展示在页面上

最开始我想根据任务调度去实现,每条服务监测数据对应一个调度器,根据设置的周期实时去该 cron 表达式

具体操作参考下面这篇博客

后面发现行不通,cron 表达式不能做到每多少秒执行一次,如每 200 秒执行一次

cron 表达式只能取近似值,每 3 分钟执行一次,像 0/200 * * * * ?,这样的表达式是不合法的

本文介绍如何使用 JDK 中提供的 Scheduled 线程池(ScheduledThreadPoolExecutor)实现我上面描述的场景

ScheduledThreadPoolExecutor

创建一个动态任务管理类,使用 ScheduledThreadPoolExecutor,如下

import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.*;

@Component
public class DynamicTaskManager {

    // 动态线程池:核心线程随任务数增长,最大上限2000
    private static final int MAX_THREADS = 2000;

    private final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(
            0,  // 初始核心线程为0
            r -> {
                Thread t = new Thread(r);
                t.setName("dynamic-task-" + t.getId());
                t.setDaemon(false);
                return t;
            }
    );

    // 任务注册表:key=任务唯一标识(比如serviceId),value=任务句柄
    private final Map<String, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>();

    /**
     * 启动或更新一个任务
     */
    public void startOrUpdateTask(String taskId, long intervalSeconds, Runnable task) {
        // 先停掉旧任务
        stopTask(taskId);

        // 动态调整核心线程数:每个任务对应一个核心线程,最大不超过2000
        int currentTaskCount = taskMap.size() + 1;
        int poolSize = Math.min(currentTaskCount, MAX_THREADS);
        scheduler.setCorePoolSize(poolSize);
        scheduler.setKeepAliveTime(60, TimeUnit.SECONDS);
        scheduler.allowCoreThreadTimeOut(true);

        // 提交新任务
        ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
                task,
                0,
                intervalSeconds,
                TimeUnit.SECONDS
        );

        taskMap.put(taskId, future);
    }

    /**
     * 停止任务
     */
    public void stopTask(String taskId) {
        ScheduledFuture<?> future = taskMap.remove(taskId);
        if (future != null && !future.isDone()) {
            future.cancel(false);
        }

        // 任务减少时,相应调整核心线程数
        int currentTaskCount = taskMap.size();
        scheduler.setCorePoolSize(currentTaskCount);
    }

    /**
     * 应用关闭时优雅停止所有任务
     */
    @PreDestroy
    public void shutdown() {
        scheduler.shutdown();
        taskMap.clear();
    }
}

使用起来就很简单了,在项目启动时,取读取所有的服务监测表,初始化这些任务,加到管理器中

页面上每次新增服务监测时,或者更新/删除服务监测时,同步维护这个任务管理器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    @Autowired
    private DynamicTaskManager taskManager;

    @PostMapping("/start")
    public Map<String, Object> startTask(@RequestParam String taskId,
                                          @RequestParam(defaultValue = "5") long intervalSeconds) {
        Map<String, Object> result = new HashMap<>();
        try {
            SampleTask task = new SampleTask(taskId);
            taskManager.startOrUpdateTask(taskId, intervalSeconds, task);
            result.put("success", true);
            result.put("message", "任务 [" + taskId + "] 已启动,间隔: " + intervalSeconds + "秒");
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "启动任务失败: " + e.getMessage());
        }
        return result;
    }

    @PostMapping("/stop")
    public Map<String, Object> stopTask(@RequestParam String taskId) {
        Map<String, Object> result = new HashMap<>();
        try {
            taskManager.stopTask(taskId);
            result.put("success", true);
            result.put("message", "任务 [" + taskId + "] 已停止");
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "停止任务失败: " + e.getMessage());
        }
        return result;
    }

    @PostMapping("/update")
    public Map<String, Object> updateTask(@RequestParam String taskId,
                                           @RequestParam long intervalSeconds) {
        Map<String, Object> result = new HashMap<>();
        try {
            SampleTask task = new SampleTask(taskId);
            taskManager.startOrUpdateTask(taskId, intervalSeconds, task);
            result.put("success", true);
            result.put("message", "任务 [" + taskId + "] 已更新,新间隔: " + intervalSeconds + "秒");
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "更新任务失败: " + e.getMessage());
        }
        return result;
    }
}

测试

写个简单任务

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class SampleTask implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(SampleTask.class);
    private final String taskId;
    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public SampleTask(String taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        String currentTime = LocalDateTime.now().format(formatter);
        logger.info("任务 [{}] 执行时间: {}", taskId, currentTime);
    }
}

启动项目,试一下

在这里插入图片描述

更新一下采集周期,改成每 10 秒执行一次

看日志,采集周期改了,完全可以

在这里插入图片描述

再加两个任务,看每个任务都能根据自己的周期去执行,非常 nice

在这里插入图片描述