SpringBoot @Async 异步 + @Scheduled 定时任务完整配置详解
一、基础概念区分
- @Scheduled:定时任务,同步阻塞执行,单线程串行执行多个定时任务,一个任务阻塞会拖慢其他任务。
- @Async:方法异步执行,将方法提交至独立线程池,不阻塞主线程;可搭配
@Scheduled实现异步定时任务。 - 核心注解配套开启:
@EnableScheduling:开启定时任务扫描@EnableAsync:开启异步方法扫描
第一部分:@Scheduled 定时任务完整配置
1. 开启定时任务
启动类添加 @EnableScheduling
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 开启定时任务支持
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args);
}
}
2. @Scheduled 四种执行规则
(1)fixedRate:固定频率(不管任务是否执行完,间隔固定毫秒再次执行)
// 每2秒执行一次,上一次没执行完,2秒后照样新开线程(同步模式下会排队阻塞)
@Scheduled(fixedRate = 2000)
public void taskRate() {
System.out.println("fixedRate定时:" + System.currentTimeMillis());
try {
Thread.sleep(3000); // 任务耗时3秒 > 间隔2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
(2)fixedDelay:固定延迟(上一次执行完成后,再等待指定毫秒执行下一次,最常用)
// 任务执行完毕后,等待3秒再执行下一次
@Scheduled(fixedDelay = 3000)
public void taskDelay() {
System.out.println("fixedDelay定时:" + System.currentTimeMillis());
}
(3)initialDelay:首次启动延迟
搭配 fixedRate/fixedDelay 使用,项目启动后延迟N毫秒才第一次执行
// 项目启动5秒后第一次执行,之后每2秒执行一次
@Scheduled(fixedRate = 2000, initialDelay = 5000)
public void taskInitDelay() {
}
(4)cron 表达式(自定义复杂时间,最灵活)
格式:秒 分 时 日 月 周 年(可选)
字段范围:
- 秒:0-59
- 分:0-59
- 时:0-23
- 日:1-31
- 月:1-12 / JAN-DEC
- 周:1-7 / SUN-SAT(1=周日,7=周六)
- 年:可选 1970-2099
常用示例:
// 每10秒执行一次
@Scheduled(cron = "0/10 * * * * ?")
// 每天凌晨2点执行
@Scheduled(cron = "0 0 2 * * ?")
// 每周一早上9点30分
@Scheduled(cron = "0 30 9 ? * MON")
// 每月1号凌晨1点
@Scheduled(cron = "0 0 1 1 * ?")
public void cronTask() {
}
注意:cron 日和周不能同时指定,其中一个必须用
?占位
3. 配置文件外部化 cron(推荐,不用改代码重启)
application.yml
task:
cron: 0/5 * * * * ?
delay: 3000
代码读取配置:
// 读取yml中的cron表达式
@Scheduled(cron = "${task.cron}")
public void configCronTask(){}
// 读取延迟数值
@Scheduled(fixedDelayString = "${task.delay}")
public void configDelayTask(){}
4. 默认定时任务线程池缺陷(重点)
Spring 默认只提供单线程执行所有 @Scheduled 任务:
- 多个定时任务串行执行
- 某一个任务耗时过长、阻塞、异常,会导致其他任务全部延迟、错过执行时机
解决方案:自定义定时任务线程池(TaskScheduler)
自定义定时任务线程池配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class SchedulePoolConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 核心线程数,同时并行执行10个定时任务
scheduler.setThreadNamePrefix("schedule-task-"); // 线程名前缀
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 关闭时等待任务执行完
scheduler.setAwaitTerminationSeconds(60); // 等待超时60秒
return scheduler;
}
}
第二部分:@Async 异步配置(单独异步 + 搭配定时任务异步)
1. 开启异步支持
启动类追加 @EnableAsync
@SpringBootApplication
@EnableScheduling
@EnableAsync // 开启异步
public class ScheduleApplication {}
2. @Async 基础使用
2.1 无返回值异步
@Service
public class AsyncService {
@Async // 该方法异步执行
public void asyncMethod() {
System.out.println("异步线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 有返回值异步(Future接收结果)
@Async
public Future<String> asyncReturn() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new AsyncResult<>("异步执行完成");
}
// 调用
@Autowired
private AsyncService asyncService;
public void test() throws ExecutionException, InterruptedException {
Future<String> future = asyncService.asyncReturn();
String res = future.get(); // 阻塞获取结果
}
3. 自定义异步线程池(必配,默认线程池性能差)
Spring 默认异步线程池 SimpleAsyncTaskExecutor 无上限,并发高会无限创建线程,必须自定义 ThreadPoolTaskExecutor
异步线程池配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncPoolConfig {
@Bean("asyncTaskExecutor") // 指定线程池名称,可在@Async指定使用
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(8);
// 最大线程数
executor.setMaxPoolSize(20);
// 队列容量
executor.setQueueCapacity(100);
// 线程前缀
executor.setThreadNamePrefix("async-thread-");
// 空闲线程存活时间
executor.setKeepAliveSeconds(60);
// 拒绝策略:任务满了由调用者线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
指定使用自定义线程池
// 使用名为 asyncTaskExecutor 的线程池执行
@Async("asyncTaskExecutor")
public void customPoolAsync(){}
4. @Scheduled + @Async 组合:异步定时任务
场景:定时任务执行耗时久,不想阻塞其他定时任务,让当前定时任务独立异步执行。
写法1:方法上加 @Async
@Service
public class AsyncScheduleTask {
// 每5秒执行,且任务异步执行
@Scheduled(fixedRate = 5000)
@Async("asyncTaskExecutor") // 使用自定义异步线程池
public void asyncScheduleTask() {
System.out.println("异步定时任务线程:" + Thread.currentThread().getName());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行逻辑说明
- 定时任务调度线程池(TaskScheduler)触发定时
- 检测到
@Async,将方法提交至异步线程池执行 - 调度线程立刻释放,不会阻塞下一次定时触发
- 多个定时任务互不影响,任务内部耗时不影响调度频率
5. @Async 常见限制(避坑)
- 不能同类内部调用
同一个类中this.asyncMethod()调用异步方法,注解失效,不会异步。
解决:通过ApplicationContext/ 自身注入@Autowired private AsyncService self;调用。 - 方法必须是
public,private/protected 不生效 - 静态方法加
@Async失效 - 异步方法内部事务失效(独立线程,事务上下文隔离)
第三部分:定时任务异常处理
1. 同步定时任务异常
默认抛出异常后,仅当前任务终止,不影响下一次调度。
2. 异步定时任务异常
无返回值的 @Async 方法异常会直接丢失日志,需要全局捕获:
方案1:自定义 AsyncUncaughtExceptionHandler
@Component
public class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
System.err.println("异步任务异常:" + method.getName() + ",异常:" + ex.getMessage());
ex.printStackTrace();
}
}
注入到异步线程池:
executor.setAsyncUncaughtExceptionHandler(new GlobalAsyncExceptionHandler());
方案2:try-catch 包裹任务逻辑(最简单)
@Scheduled(fixedRate = 3000)
@Async
public void task() {
try {
// 业务逻辑
} catch (Exception e) {
// 日志记录、告警
}
}
第四部分:完整整合示例(可直接复制运行)
1. 启动类
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class ScheduleDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleDemoApplication.class, args);
}
}
2. 定时任务线程池配置 ScheduleConfig
@Configuration
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(8);
scheduler.setThreadNamePrefix("schedule-pool-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
}
3. 异步线程池 AsyncConfig
@Configuration
public class AsyncConfig {
@Bean("businessAsyncPool")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("business-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setAsyncUncaughtExceptionHandler(new GlobalAsyncExceptionHandler());
executor.initialize();
return executor;
}
}
4. 定时任务业务类
@Service
public class ScheduleTaskService {
// 同步定时,多任务会串行阻塞
@Scheduled(fixedDelay = 2000)
public void syncTask() {
System.out.println("同步定时任务:" + Thread.currentThread().getName());
}
// 异步定时,独立线程执行,不阻塞调度
@Scheduled(cron = "0/5 * * * * ?")
@Async("businessAsyncPool")
public void asyncTask() {
try {
System.out.println("异步定时任务:" + Thread.currentThread().getName());
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
第五部分:生产环境最佳实践
- 必须自定义 TaskScheduler:扩大定时调度线程数,避免单线程阻塞所有任务
- 必须自定义 Async 线程池:控制并发,防止无限创建线程 OOM
- 长耗时定时任务统一加
@Async,实现异步调度 - cron 表达式放到配置文件,方便线上动态修改无需改代码
- 所有定时、异步任务增加 try-catch + 日志告警(邮件/短信)
- 服务优雅关闭:配置
setWaitForTasksToCompleteOnShutdown(true),停机前执行完正在运行的任务 - 区分两种线程池:
- TaskScheduler:负责触发定时(调度线程)
- ThreadPoolTaskExecutor:负责执行业务(异步工作线程)