> 彻底搞懂 Java 中的定时任务,看这一篇就够了
前言
什么是定时任务?简单说就是**让程序在指定时间自动执行某个任务**。比如:
-
每天凌晨 3 点跑批处理
-
每隔 5 分钟检查订单状态
-
每周一早上 9 点发送周报
-
每月 1 号生成财务报表
这些场景,都离不开定时任务。
本文会详细介绍 Java 中实现定时任务的 **4 种主流方式**,并配上完整的代码示例,保证你看完就能上手用。
一、为什么需要定时任务?
在正式讲代码之前,先说说定时任务能做什么,了解清楚它的应用场景。
定时任务的典型应用
| 场景 | 说明 |
|------|------|
| **数据同步** | 定时从第三方 API 拉取数据到本地 |
| **报表生成** | 每天/每周/每月自动生成统计报表 |
| **缓存清理** | 定时清理过期缓存,释放内存 |
| **订单处理** | 定时检查超时未支付订单,自动取消 |
| **通知推送** | 定时推送消息、邮件、短信 |
| **数据备份** | 凌晨定时备份数据库 |
| **健康检查** | 定时检查服务是否存活 |
看到了吧,定时任务的用途非常广泛,几乎每个业务系统都离不开它。
二、Java 定时任务的 4 种实现方式
接下来进入正题,看看 Java 中怎么实现定时任务。
Timer + TimerTask(最简单)
这是 Java 自带的最原始的方式,代码简单,适合简单场景。
java
import java.util.Timer;
import java.util.TimerTask;
/**
* 方式一:使用 Timer 实现定时任务
*
* 特点:
* - JDK 自带,无需额外依赖
* - 简单易用
* - 单线程,任务会串行执行
* - 如果某个任务抛出异常,Timer 会停止所有任务
*/
public class TimerDemo {
public static void main(String[] args) {
// 创建一个 Timer 实例
Timer timer = new Timer();
// 1. 延迟 3 秒后执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务执行了!当前时间:" + System.currentTimeMillis());
}
}, 3000); // 3000 毫秒 = 3 秒
// 2. 延迟 1 秒后开始,每隔 2 秒执行一次(无限次)
timer.scheduleAtFixedRate(new TimerTask() {
private int count = 0;
@Override
public void run() {
count++;
System.out.println("第 " + count + " 次执行,时间:" + System.currentTimeMillis());
}
}, 1000, 2000); // 延迟 1 秒,间隔 2 秒
// 注意:如果想停止任务,调用 timer.cancel()
// timer.cancel();
}
}
java
**运行结果:**
任务执行了!当前时间:1647834000000
第 1 次执行,时间:1647834001000
第 2 次执行,时间:1647834003000
第 3 次执行,时间:1647834005000
schedule() vs scheduleAtFixedRate() 的区别:
-
`schedule()`:下一次任务开始时间 = 上一次任务结束时间 + 间隔
-
`scheduleAtFixedRate()`:下一次任务开始时间 = 上一次任务开始时间 + 间隔(固定频率)
简单说,如果任务执行时间超过了间隔,schedule 会"追不上",scheduleAtFixedRate 会"追赶"。
ScheduledExecutorService(推荐使用)
这是"JDK 1.5+"提供的更强大的方式,**线程池**实现,更稳定、更高效。
java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 方式二:使用 ScheduledExecutorService 实现定时任务
*
* 特点:
* - JDK 1.5+ 提供,无需额外依赖
* - 多线程并行执行,互不影响
* - 某个任务抛出异常,不影响其他任务
* - 官方推荐使用
*/
public class ScheduledExecutorDemo {
public static void main(String[] args) {
// 创建一个线程池,核心线程数为 2
// 这里的 2 表示最多有 2 个线程同时执行定时任务
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
// 任务 1:延迟 3 秒后执行一次
scheduler.schedule(() -> {
System.out.println("任务1执行了,延迟3秒");
}, 3, TimeUnit.SECONDS);
// 任务 2:延迟 1 秒后开始,每隔 2 秒执行一次(无限次)
// 这里用了 Lambda 表达式,更简洁
scheduler.scheduleAtFixedRate(() -> {
System.out.println("任务2执行了,时间:" + System.currentTimeMillis());
}, 1, 2, TimeUnit.SECONDS);
// 任务 3:延迟 5 秒后开始,每隔 3 秒执行一次,执行 5 次后停止
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("任务3执行了");
}, 5, 3, TimeUnit.SECONDS);
// 关闭 scheduler(通常在应用关闭时调用)
// scheduler.shutdown();
}
}
scheduleAtFixedRate() vs scheduleWithFixedDelay():
java
// scheduleAtFixedRate:固定频率
// 上次开始时间 + 间隔 = 下次开始时间
// 适合:任务执行时间 < 间隔时间的场景
// scheduleWithFixedDelay:固定延迟
// 上次结束时间 + 间隔 = 下次开始时间
// 适合:任务执行时间不确定,或者任务之间需要隔离的场景
Spring @Scheduled(Spring 用户的首选)
如果你使用 Spring Framework,这个注解就是你的首选。配置简单,注解驱动。
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 方式三:Spring @Scheduled 定时任务
*
* 使用步骤:
* 1. 在 Spring Boot 主类上添加 @EnableScheduling 注解
* 2. 在方法上添加 @Scheduled 注解
* 3. 方法需要是无参数、无返回值的
*/
@Component
public class SpringScheduledDemo {
/**
* 每隔 5 秒执行一次
* fixedRate:从上一次开始时间计算间隔
*/
@Scheduled(fixedRate = 5000)
public void task1() {
System.out.println("每5秒执行一次 - " + System.currentTimeMillis());
}
/**
* 每隔 3 秒执行一次
* fixedDelay:从上一次结束时间计算间隔
*/
@Scheduled(fixedDelay = 3000)
public void task2() {
System.out.println("每3秒执行一次(延迟) - " + System.currentTimeMillis());
}
/**
* 初始延迟 10 秒后开始,然后每隔 5 秒执行
*/
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void task3() {
System.out.println("延迟10秒后每5秒执行 - " + System.currentTimeMillis());
}
/**
* Cron 表达式:每秒执行
* Cron 格式:秒 分 时 日 月 周 [年]
*/
@Scheduled(cron = "* * * * * ?")
public void task4() {
System.out.println("每秒执行一次 - " + System.currentTimeMillis());
}
/**
* Cron 表达式:每天凌晨 3 点执行
*/
@Scheduled(cron = "0 0 3 * * ?")
public void task5() {
System.out.println("每天凌晨3点执行");
}
/**
* Cron 表达式:每周一早上 9 点执行
*/
@Scheduled(cron = "0 0 9 ? * MON")
public void task6() {
System.out.println("每周一早上9点执行");
}
/**
* Cron 表达式:每月的 1 号凌晨 0 点执行
*/
@Scheduled(cron = "0 0 0 1 * ?")
public void task7() {
System.out.println("每月1号凌晨0点执行");
}
}
Spring Boot 启动类配置:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 开启定时任务功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Scheduled 完整参数说明:
java
@Scheduled(
// 固定间隔(毫秒),从上次开始计时
fixedRate = 1000,
// 固定延迟(毫秒),从上次结束计时
fixedDelay = 1000,
// 初始延迟(毫秒),首次执行前等待时间
initialDelay = 1000,
// Cron 表达式(和 fixedRate 等互斥)
cron = "0/5 * * * * ?",
// 时区
zone = "Asia/Shanghai"
)
public void doTask() {
// 任务逻辑
}
常用 Cron 表达式示例:
| 表达式 | 含义 |
|--------|------|
| `0/5 * * * * ?` | 每 5 秒 |
| `0 0/1 * * * ?` | 每分钟 |
| `0 0 * * * ?` | 每小时 |
| `0 0 0 * * ?` | 每天凌晨 |
| `0 0 12 * * ?` | 每天中午 12 点 |
| `0 0 10,14,16 * * ?` | 每天 10、14、16 点 |
| `0 0/30 9-17 * * ?` | 9 点到 17 点,每 30 分钟 |
| `0 0 12 ? * WED` | 每周三中午 12 点 |
| `0 0 12 1 * ?` | 每月 1 号中午 12 点 |
| `0 15 10 L * ?` | 每月最后一天上午 10:15 |
Quartz(企业级定时任务)
如果你需要**动态管理任务**、**集群部署**、**任务依赖**等复杂场景,Quartz 是更好的选择。
java
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
/**
* 方式四:Quartz 定时任务框架
*
* 特点:
* - 功能最强大,支持动态管理任务
* - 支持集群、分布式
* - 支持任务依赖
* - 需要引入额外依赖
*/
public class QuartzDemo {
public static void main(String[] args) throws SchedulerException {
// 1. 创建调度器(Scheduler)
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 2. 定义任务(Job)
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "group1") // 任务名和组名
.usingJobData("message", "Hello Quartz") // 传递参数
.build();
// 3. 定义触发器(Trigger)
// 每隔 5 秒执行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startNow() // 立即开始
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5) // 间隔 5 秒
.repeatForever()) // 无限次执行
.build();
// 4. 将任务和触发器关联到调度器
scheduler.scheduleJob(job, trigger);
// 5. 启动调度器
scheduler.start();
// 关闭调度器
// scheduler.shutdown();
}
}
/**
* 自定义任务类
* 需要实现 Job 接口
*/
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 获取传递的参数
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String message = dataMap.getString("message");
// 执行业务逻辑
System.out.println("Quartz 任务执行了!message = " + message);
System.out.println("执行时间:" + System.currentTimeMillis());
}
}
Quartz 在 Spring Boot 中的使用:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
/**
* Quartz 配置类(Spring Boot 整合)
*/
@Configuration
public class QuartzConfig {
/**
* 配置 JobDetail
*/
@Bean
public JobDetailFactoryBean myJobDetail() {
JobDetailFactoryBean factory = new JobDetailFactoryBean();
factory.setJobClass(MyQuartzJob.class);
factory.setDurability(true);
return factory;
}
/**
* 配置 Trigger
*/
@Bean
public SimpleTriggerFactoryBean myTrigger(JobDetailFactoryBean myJobDetail) {
SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
factory.setJobDetail(myJobDetail.getObject());
factory.setRepeatInterval(5000); // 间隔 5 秒
factory.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
return factory;
}
}
三、4 种方式的对比
| 特性 | Timer | ScheduledExecutorService | Spring @Scheduled | Quartz |
|--------|---------|--------------------------------------|----------------------------|-----------|
| **使用难度** | ⭐ 简单 | ⭐ 简单 | ⭐ 简单 | ⭐⭐ 较复杂 |
| **依赖** | JDK 自带 | JDK 1.5+ | Spring Framework | 需引入依赖 |
| **多线程** | ❌ 单线程 | ✅ 多线程 | ✅ 多线程 | ✅ 多线程 |
| **异常处理** | 会导致全部停止 | 不影响其他任务 | 不影响其他任务 | 不影响其他任务 |
| **动态管理** | ❌ 不支持 | ❌ 不支持 | ⚠️ 支持但不灵活 | ✅ 强大支持 |
| **集群/分布式** | ❌ 不支持 | ❌ 不支持 | ⚠️ 需额外配置 | ✅ 原生支持 |
| **适用场景** | 简单定时任务 | 较复杂定时任务 | Spring 项目日常使用 | 企业级复杂任务 |
四、实际业务场景示例
场景 1:每天凌晨同步数据
java
@Scheduled(cron = "0 0 2 * * ?")
public void syncData() {
log.info("开始同步数据...");
try {
// 1. 从第三方 API 获取数据
List<Data> dataList = externalApi.fetchData();
// 2. 处理数据
List<Data> processed = processor.process(dataList);
// 3. 保存到数据库
repository.saveAll(processed);
log.info("数据同步完成,共处理 {} 条数据", processed.size());
} catch (Exception e) {
log.error("数据同步失败", e);
// 可以发送告警通知
notifyService.alert("数据同步失败:" + e.getMessage());
}
}
场景 2:定期清理过期订单
java
@Scheduled(fixedDelay = 300000) // 每 5 分钟执行一次
public void cleanExpiredOrders() {
log.info("开始清理过期订单...");
// 查询超过 30 分钟未支付的订单
List<Order> expiredOrders = orderRepository
.findByStatusAndCreateTimeBefore(
OrderStatus.PENDING,
LocalDateTime.now().minusMinutes(30)
);
for (Order order : expiredOrders) {
order.setStatus(OrderStatus.CANCELLED);
order.setCancelReason("超时未支付,自动取消");
}
orderRepository.saveAll(expiredOrders);
log.info("已清理 {} 条过期订单", expiredOrders.size());
}
场景 3:心跳检测
java
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void healthCheck() {
log.info("开始健康检查...");
// 检查各个服务是否可用
boolean apiOk = checkService("user-api");
boolean paymentOk = checkService("payment-api");
boolean orderOk = checkService("order-api");
if (!apiOk || !paymentOk || !orderOk) {
// 发送告警
String msg = String.format("服务异常: user-api=%s, payment-api=%s, order-api=%s",
apiOk ? "OK" : "FAIL",
paymentOk ? "OK" : "FAIL",
orderOk ? "OK" : "FAIL");
alertService.sendAlert(msg);
}
}
private boolean checkService(String serviceName) {
try {
return healthEndpoint.check(serviceName).isHealthy();
} catch (Exception e) {
log.warn("检查服务 {} 失败", serviceName, e);
return false;
}
}
五、注意事项
1. 时区问题
java
// 默认使用服务器时区,注意 Cron 表达式的时区
@Scheduled(cron = "0 0 9 * * ?") // 服务器时区早上9点
// 指定时区
@Scheduled(cron = "0 0 9 * * ?", zone = "Asia/Shanghai") // 上海时区早上9点
2. 任务执行时间超过间隔
java
// 如果任务执行时间是 8 秒,间隔是 5 秒
// schedule:任务完成后等 5 秒再执行下一次
// scheduleAtFixedRate:立即执行下一次(会"追赶")
// 建议:任务执行时间 > 间隔时间时,使用 scheduleWithFixedDelay
@Scheduled(fixedDelay = 5000) // 上次结束后等 5 秒
3. 集群环境下的定时任务
在多实例部署时,如果不处理,多个实例会同时执行同一个定时任务,导致数据问题。
解决方案:
-
**Quartz 集群**:自带分布式锁
-
**Spring @Scheduled + Redis 分布式锁**
-
**数据库行锁**
-
**分布式调度中间件**:XXL-Job、ElasticJob
4. 优雅关闭
java
@PreDestroy
public void shutdown() {
log.info("关闭定时任务调度器...");
if (scheduler != null) {
scheduler.shutdown();
}
}
六、总结
-
**简单场景**:用 `Timer` 或 `ScheduledExecutorService`
-
**Spring 项目**:用 `@Scheduled` 注解,最方便
-
**复杂企业级**:用 `Quartz`,功能最强大
-
**集群环境**:选 `Quartz` 或分布式调度框架
定时任务是后端开发的基本功,面试也经常问。
建议大家**自己动手写一遍**,跑跑代码,感受一下不同方式的区别。纸上得来终觉浅,绝知此事要躬行。
如果觉得有帮助,**点赞 + 收藏 + 关注**,我们下期再见!