
本文介绍通过共享同步执行器(synchronized 方法)协调多个 @scheduled 定时任务,确保 job1 与 job2 互斥运行,并支持自定义任务间延迟,避免并发冲突。
本文介绍通过共享同步执行器(synchronized 方法)协调多个 @scheduled 定时任务,确保 job1 与 job2 互斥运行,并支持自定义任务间延迟,避免并发冲突。
在 Spring 应用中,当多个 @Scheduled 批处理任务(如 Job1 和 Job2)以相近周期触发时,若其业务逻辑存在资源竞争或依赖顺序(例如共享数据库连接、写入同一文件、调用限流外部接口),直接并行执行极易引发数据不一致、超时或失败。虽然你为两个任务设置了相同的 fixedRate = 20000(20 秒),但 Spring 默认使用线程池异步调度——这意味着 Job1 和 Job2 可能同时启动,无法满足“一个完成后再等若干秒才执行另一个”的串行+延迟要求。
解决该问题的核心思路是将并发控制权从调度层上移至业务执行层:不再依赖调度时间错开,而是让所有任务统一经由一个线程安全的执行门控器(Runner)串行化执行,并在每次任务切换后插入可控延迟。
以下是一个生产就绪的实现方案:
✅ 步骤一:定义线程安全的任务执行器
@Component
public class SingleThreadJobRunner {
private final long interJobDelayMs; // 可配置的任务间延迟(毫秒)
public SingleThreadJobRunner(@Value("${job.inter-delay-ms:500}") long interJobDelayMs) {
this.interJobDelayMs = interJobDelayMs;
}
public synchronized void run(Runnable task) {
try {
task.run();
// 任务成功执行后,主动等待指定延迟(仅在本次执行结束、下一次任一任务开始前生效)
if (interJobDelayMs > 0) {
Thread.sleep(interJobDelayMs);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Job execution interrupted", e);
}
}
}
? 注意:
synchronized保证同一时刻仅有一个任务在run()内执行;Thread.sleep()插入的延迟发生在当前任务结束之后、下一个被调度任务真正开始之前,完美契合“Job1 完成 → 等 X 秒 → Job2 启动”的语义。
✅ 步骤二:改造定时任务,委托给执行器
@Component
@RequiredArgsConstructor
public class Job1 {
private final SingleThreadJobRunner runner;
@Scheduled(fixedRate = 20000) // 仍保持 20 秒周期,但实际执行受 runner 控制
public void execute() {
runner.run(() -> {
System.out.println("[" + LocalDateTime.now() + "] Job1 STARTED");
// ? 模拟业务逻辑(如数据库操作、HTTP 调用等)
try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("[" + LocalDateTime.now() + "] Job1 COMPLETED");
});
}
}
@Component
@RequiredArgsConstructor
public class Job2 {
private final SingleThreadJobRunner runner;
@Scheduled(fixedRate = 20000)
public void execute() {
runner.run(() -> {
System.out.println("[" + LocalDateTime.now() + "] Job2 STARTED");
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("[" + LocalDateTime.now() + "] Job2 COMPLETED");
});
}
}
✅ 关键优势与注意事项
-
零竞态、强串行:
synchronized天然保障临界区独占,无需额外锁或分布式协调; -
延迟可配:通过
@Value注入interJobDelayMs,支持运行时调整(如application.yml中配置job.inter-delay-ms: 3000); -
失败隔离:任一任务抛异常不会阻塞
runner,后续任务仍可正常排队执行(建议在run()内增加日志和监控埋点); -
⚠️ 避免在
run()中执行耗时 I/O 或阻塞操作:否则会拖慢整个队列;应将长耗时逻辑放入Runnable内部,而非runner方法体; -
扩展性提示:如需支持优先级、超时熔断或异步回调,可将
synchronized替换为ReentrantLock+Condition,或升级为基于BlockingQueue的任务队列。
通过该设计,你彻底解耦了“调度节奏”与“执行顺序”,既保留了 @Scheduled 的简洁性,又获得了精细的串行控制能力——这才是面向生产环境的稳健实践。










