SingleThreadScheduledExecutor定时任务逻辑异常排查与优化咨询
问题分析与解决方案
嘿,我一眼就揪出了你代码里的核心问题,咱们一步步拆解:
1. 致命的时间初始化漏洞
你在run()方法开头只创建了一次Calendar cal = Calendar.getInstance();,这个对象的时间会永远停留在程序启动的那一刻!之后每次scheduleAtFixedRate触发任务时,用的都是这个旧时间,这直接导致了你的两个bug:
- 若7:00启动程序,
cal的时间固定为7:00,哪怕到了20:30,判断时仍认为处于有效时段,所以会继续执行任务; - 若在无效时段(比如21:00)启动,
cal的时间固定为21:00,哪怕之后到了第二天8:00,判断时还是用21:00的时间,所以一直记录错误日志,不会执行任务。
2. 其他次要问题
running变量完全没用:你设置了running=false,但scheduleAtFixedRate会持续每隔30分钟触发任务,这个变量根本不会影响调度逻辑;- 每次调用
run()都会创建新的SingleThreadScheduledExecutor,如果多次调用这个方法,会造成线程泄漏; Runtime.exec()没有处理进程的输入/输出流,可能导致子进程阻塞(比如当进程输出较多时)。
修正后的基础代码
先把核心问题修复,同时优化其他细节:
public final void startScheduler() { // 重命名方法更清晰,避免和Runnable的run混淆 // 只创建一次调度器,避免重复创建导致资源泄漏 if (exec != null && !exec.isShutdown()) { return; } exec = Executors.newSingleThreadScheduledExecutor(); exec.scheduleAtFixedRate(new Runnable() { public void run() { // 每次执行任务时都重新获取当前时间! final Calendar cal = Calendar.getInstance(); try { int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); int hour = cal.get(Calendar.HOUR_OF_DAY); // 用Calendar常量替代魔法数字,可读性更高 if (dayOfWeek == Calendar.SUNDAY || dayOfWeek == Calendar.SATURDAY) { Globals.addMessage("Non-scheduled work day, please try again later."); Globals.addMessage("Process aborted."); } else if (hour < 6 || hour >= 20) { // 改成>=20,避免20:00之后执行任务 Globals.addMessage("Non-scheduled work hours, please try again later."); Globals.addMessage("Process aborted."); } else { // 用ProcessBuilder替代Runtime.exec,更可控 ProcessBuilder pb = new ProcessBuilder(ServerVar.getVariable(ServerVars.PATH)); Process process = pb.start(); // 处理子进程的输入输出,避免阻塞 new Thread(() -> { try (InputStream in = process.getInputStream()) { in.transferTo(System.out); } catch (IOException e) { Globals.addMessage(e.toString()); } }).start(); new Thread(() -> { try (InputStream err = process.getErrorStream()) { err.transferTo(System.err); } catch (IOException e) { Globals.addMessage(e.toString()); } }).start(); Globals.addMessage("Program is running."); } } catch (IOException e) { Globals.addMessage(e.toString()); } } }, 0, 30, TimeUnit.MINUTES); } // 记得添加关闭调度器的方法,避免程序退出时资源泄漏 public void stopScheduler() { if (exec != null && !exec.isShutdown()) { exec.shutdown(); try { if (!exec.awaitTermination(1, TimeUnit.MINUTES)) { exec.shutdownNow(); } } catch (InterruptedException e) { exec.shutdownNow(); } } }
更高效的实现方式(Java 8+)
如果你的项目用的是Java 8或更高版本,推荐使用java.timeAPI替代老旧的Calendar,代码更简洁易读:
import java.time.DayOfWeek; import java.time.LocalTime; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; public class TaskScheduler { private ScheduledExecutorService exec; public final void startScheduler() { if (exec != null && !exec.isShutdown()) { return; } exec = Executors.newSingleThreadScheduledExecutor(); exec.scheduleAtFixedRate(this::executeTask, 0, 30, TimeUnit.MINUTES); } private void executeTask() { DayOfWeek currentDay = DayOfWeek.now(); LocalTime currentTime = LocalTime.now(); // 判断是否为周末 if (currentDay == DayOfWeek.SATURDAY || currentDay == DayOfWeek.SUNDAY) { logAbort("Non-scheduled work day"); return; } // 判断是否在工作时段内 LocalTime workStart = LocalTime.of(6, 0); LocalTime workEnd = LocalTime.of(20, 0); if (currentTime.isBefore(workStart) || currentTime.isAfter(workEnd)) { logAbort("Non-scheduled work hours"); return; } try { ProcessBuilder pb = new ProcessBuilder(ServerVar.getVariable(ServerVars.PATH)); Process process = pb.start(); // 统一处理进程流 handleProcessStream(process.getInputStream(), System.out); handleProcessStream(process.getErrorStream(), System.err); Globals.addMessage("Program is running."); } catch (IOException e) { Globals.addMessage(e.toString()); } } private void logAbort(String reason) { Globals.addMessage(reason + ", please try again later."); Globals.addMessage("Process aborted."); } private void handleProcessStream(InputStream in, OutputStream out) { new Thread(() -> { try (in; out) { // try-with-resources自动关闭流 in.transferTo(out); } catch (IOException e) { Globals.addMessage(e.toString()); } }).start(); } public void stopScheduler() { if (exec != null && !exec.isShutdown()) { exec.shutdown(); try { if (!exec.awaitTermination(1, TimeUnit.MINUTES)) { exec.shutdownNow(); } } catch (InterruptedException e) { exec.shutdownNow(); } } } }
进阶建议:使用专业调度框架
如果你的调度需求更复杂(比如需要排除节假日、动态调整执行时间等),推荐使用Quartz或者Spring Scheduler,这些框架提供了更强大的调度能力,比如可以直接用 cron 表达式定义执行规则:
// Spring Scheduler 示例(需Spring环境) @Scheduled(cron = "0 0/30 6-19 * * MON-FRI") // 工作日6:00-19:30,每30分钟执行一次 public void scheduledTask() { try { ProcessBuilder pb = new ProcessBuilder(ServerVar.getVariable(ServerVars.PATH)); Process process = pb.start(); // 处理进程流... Globals.addMessage("Program is running."); } catch (IOException e) { Globals.addMessage(e.toString()); } }
这个cron表达式0 0/30 6-19 * * MON-FRI直接定义了“工作日(周一到周五)的6点到19点59分,每30分钟执行一次”,完全不需要手动判断时间,简洁又可靠。
内容的提问来源于stack exchange,提问作者M. Winnicki
相关产品推荐
相关产品推荐

