Spring定时任务和@Async注解异步调用
Spring定时任务
1、@Scheduled注解方式
使用方式
-
@Scheduled的使用方式十分简单,首先在项目启动类添加注解@EnableScheduled。
-
编写定时任务方法,方法上添加注解@Scheduled。
-
如果有多个定时任务,可以使用异步或者多线程解决。
参数说明
@Scheduled(fixedRate=2000):上一次开始执行时间点后2秒再次执行;单位ms;
@Scheduled(fixedDelay=2000):上一次执行完毕时间点后2秒再次执行;单位ms;
@Scheduled(initialDelay=1000, fixedDelay=2000):第一次延迟1秒执行,然后在上一次执行完毕时间点后2秒再次执行;
@Scheduled(cron="* * * * * ?"):按cron规则执行。
注解方式总结
-
如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay;
-
如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式。
参考代码示例
@Component
public class testTask {
private Logger logger = LoggerFactory.getLogger(testTask.class);
@Scheduled(cron = "0/5 * * * * ?")
public void doTask() {
logger.info(Thread.currentThread().getName()+"===task run");
}
}
按条件自动停止任务
@Slf4j
@Component
public class AutoStopTask {
@Autowired
private CustomTaskScheduler customTaskScheduler;
private int count;
@Scheduled(cron = "*/3 * * * * *")
public void printTask() {
log.info("任务执行次数:{}", count + 1);
count++;
// 执行3次后自动停止
if (count >= 3) {
log.info("任务已执行指定次数,现在自动停止");
boolean cancelled = customTaskScheduler.getScheduledTasks().get(this).cancel(true);
// 停止后再次启动
if (cancelled) {
count = 0;
ScheduledMethodRunnable runnable = new ScheduledMethodRunnable(this, ReflectionUtils.findMethod(this.getClass(), "printTask"));
customTaskScheduler.schedule(runnable, new CronTrigger("*/3 * * * * *"));
}
}
}
}
Cron表达式参数详解
关于cron表达式的写法
如:cron="* * * * * ?"
按顺序依次为:1~7(最后一位“年”可省略)
序号 | 含义 | 设值范围 | 通配符 |
---|---|---|---|
1 | 秒 | (0~59) | , - * / |
2 | 分钟 | (0~59) | , - * / |
3 | 小时 | (0~23) | , - * / |
4 | 天 | (1~31) | , - * ? / L W C |
5 | 月 | (1~12) | , - * / |
6 | 星期 | (1~7 1=星期日 7=星期六) | , - * ? / L C # |
7 | 年份(可选) | (1970-2099) | , - * / |
在线Cron表达式生成器:http://cron.qqe2.com/
表达式参数说明
, # 表示一个列表(1,3,5),用逗号拼接具体的时间值
- # 表示一个连续区间(9-12)
* # 代表所有可能的值
? # 仅被用于天和星期两个子表达式,表示不指定值
/ # 用来指定数值的增量(分钟里的“0/15”表示从第0分钟开始,每隔15分钟)
# # 用来指定具体的周数,"#"前面代表星期,"#"后面代表本月第几周
# 比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四
# 因此,"5L"这种形式只不过是"#"的特殊形式而已
L # 仅被用于天和星期两个子表达式,它是单词“last”的缩写,“L”表示这个月的最后一日,“6L”表示这个月的倒数第6天
W # 代表着*日(Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最*的一个*日。
# 大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。
# 日域中的 15W 意味着离该月15号的最*一个*日。假如15号是星期六,那么 trigger 会在14号(星期五)触发,因为星期四比星期一离15号更*。
C # 代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。
# 例如5C在“天”字段中就相当于日历5日以后的第一天。1C在“星期”字段中相当于星期日后的第一天。
由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置“?”号
常用表达式参考
"*/5 * * * * ?" # 每隔5秒执行一次
"0 */1 * * * ?" # 每隔1分钟执行一次
"0 0 23 * * ?" # 每天23点执行一次
"0 0 1 * * ?" # 每天凌晨1点执行一次
"0 0 1 1 * ?" # 每月1号凌晨1点执行一次
"0 0 23 L * ?" # 每月最后一天23点执行一次
"0 0 1 ? * L" # 每周星期天凌晨1点实行一次:
"0 26,29,33 * * * ?" # 在26分、29分、33分执行一次
"0 0 0,3,8,21 * * ?" # 每天的0点、3点、8点、21点执行一次
"0 0 10,14,16 * * ?" # 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?" # 朝九晚五工作时间内每半小时
"0 0 12 ? * WED" # 表示每个星期三中午12点
"0 0 12 * * ?" # 每天中午12点触发
"0 15 10 ? * *" # 每天上午10:15触发
"0 15 10 * * ?" # 每天上午10:15触发
"0 15 10 * * ? *" # 每天上午10:15触发
"0 15 10 * * ?" # 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" # 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" # 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" # 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" # 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" # 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" # 周一至周五的上午10:15触发
"0 15 10 15 * ?" # 每月15日上午10:15触发
"0 15 10 L * ?" # 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" # 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" # 每月的第三个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" # 2002年至2005年的每月的最后一个星期五上午10:15触发
2、执行多个定时任务
2.1、通过@Async注解异步调用
在项目启动类添加注解@EnableAsync
注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
创建自定义线程池,使用线程池异步执行多个任务
异步调用
@Component
public class Task {
@Async(value ="myPoolTaskExecutor")
public void doTaskOne() throws Exception {
// 同上内容,省略
}
@Async(value ="myPoolTaskExecutor")
public void doTaskTwo() throws Exception {
// 同上内容,省略
}
@Async(value ="myPoolTaskExecutor")
public void doTaskThree() throws Exception {
// 同上内容,省略
}
/**
* 创建自定义线程池,提供异步调用时使用
**/
@Bean(name = "myPoolTaskExecutor")
public ThreadPoolTaskExecutor getMyPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(10);
//线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(100);
//缓存队列
taskExecutor.setQueueCapacity(50);
//许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
//异步方法内部线程名称
taskExecutor.setThreadNamePrefix("poolTestThread-");
/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/
// 拒绝策略
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
System.out.println("@Async 业务处理线程配置成功,核心线程池:[{}],最大线程池:[{}],队列容量:[{}],线程名称前缀:[{}]");
return taskExecutor;
}
}
异步回调(扩展知识)
@Async(value ="myPoolTaskExecutor")
public Future<String> doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}
//注意:调用方与被调方不能在同一个类
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
Future<String> task1 = task.doTaskOne(); //异步任务1
Future<String> task2 = task.doTaskTwo(); //异步任务2
Future<String> task3 = task.doTaskThree(); //异步任务3
while(true) {
if(task1.isDone() && task2.isDone() && task3.isDone()) {
// 三个任务都调用完成,处理点其它事,退出循环等待
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
@Async不生效的解决方法
1、检查是否配置相关注解
在需要用到 @Async 注解的类上加上 @EnableAsync,或者直接加在springboot启动类上;
2、在同一个类中调用需要先获取代理对象,也就是手动获取对象。
因为 @Async 注解是基于Spring AOP (面向切面编程)的,而AOP的实现是基于动态代理模式实现的。有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
public class TestController{
@GetMapping("/testAsync")
public String testAsync() throws InterruptedException {
// 手动获取代理对象
SpringUtil.getBean(TestController.class).syncData();
return "测试异步完成";
}
@Async
public void syncData() throws InterruptedException {
System.out.println("异步方法执行......" + Thread.currentThread().getId());
}
}
3、不同的类调用,直接注入即可
public class TestController{
@Autowired
private TestService testService;
@GetMapping("/testAsync")
public String testAsync() throws InterruptedException {
testService.syncData();
return "测试异步完成";
}
}
public class TestServiceImpl{
@Async
public void syncData() throws InterruptedException {
System.out.println("异步方法执行......" + Thread.currentThread().getId());
}
}
@Async产生的异常问题
SpringBoot使用@Async导致的循环依赖报错的解决方案
2.2、通过实现SchedulingConfigurer接口
Spring 中,创建定时任务除了使用@Scheduled 注解外,还可以使用 SchedulingConfigurer。
@Schedule 注解有一个缺点,其定时的时间不能动态的改变,而基于 SchedulingConfigurer 接口的方式可以做到。
点击查看基于Spring的SchedulingConfigurer实现动态定时任务
@Configuration
public class TaskConfig implements SchedulingConfigurer {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(10);
executor.setThreadNamePrefix("task-thread");
//设置饱和策略
//CallerRunsPolicy:线程池的饱和策略之一,当线程池使用饱和后,直接使用调用者所在的线程来执行任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
//配置@Scheduled 定时器所使用的线程池
//配置任务注册器:ScheduledTaskRegistrar 的任务调度器
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
//可配置两种类型:TaskScheduler、ScheduledExecutorService
//scheduledTaskRegistrar.setScheduler(taskScheduler());
//只可配置一种类型:taskScheduler
scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
}
}
编写定时任务
//注册为spring容器的组件
@Component
@Slf4j
public class SchedulerTask {
//定时任务
// 5 * * * * ? 在每分钟的5秒执行
@Scheduled(cron = " 5 * * * * ? ")
public void scheduleTask() {
try {
log.info("定时任务: 开始执行");
//todo:执行业务
log.info("定时任务: 执行完毕");
} catch (Exception e) {
log.error("定时任务执行出错", e);
}
}
}
3、Spring线程池和Jdk线程池
- jdk线程池就是使用jdk线程工具类ThreadPoolExecutor 创建线程池
- spring线程池就是使用自己配置的线程池,然后交给spring处理,可以采用Async,也可以引入线程池的bean依赖。
记一下线程池优化不当导致的资源耗尽
3.1、JDK普通线程池
由于该接口访问频率很频繁,每次访问会创建新的线程池,另外接口的循环内部不断创建线程处理,导致访问次数多了系统内部资源耗尽。
public void taskTest(){
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(subjectList.size());
try {
for (Subject subject : subjectList) {
Member finalMember = member;
executorService.execute(() -> {
//耗时操作...
content.add(subjectMap);
countDownLatch.countDown();
});
}
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
3.2、JDK普通线程池(优化)
不用每次创建新的线程池
public class ThreadPoolTest {
private final Logger logger= LoggerFactory.getLogger(ThreadPoolTests.class);
//JDK普通线程池
private ExecutorService executorService= Executors.newFixedThreadPool(10);
private void sleep(long m){
try {
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void testExecutorService(){
Runnable task=new Runnable() {
@Override
public void run() {
logger.debug("Hello ExecutorService");
}
};
for(int i=0;i<10;i++){
//调用submit方法,线程池会分配一个线程进行执行这个任务
executorService.submit(task);
}
sleep(10000);
}
}
3.3、Spring默认的线程池SimpleAsyncTaskExecutor
Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor
在没有配置的情况下,默认使用的是simpleAsyncTaskExecutor。(Spring提供了7个线程池的实现,感兴趣的可以自行了解)
特点
每次执行任务时,它会重新启动一个新的线程,并允许开发者控制并发线程的最大数量(concurrencyLimit),从而起到一定的资源节流作用。
默认是concurrencyLimit取值为-1,即不启用资源节流。
3.4、Spring自带的线程池ThreadPoolTaskExecutor
ThreadPoolTaskExecutor类,其本质是对java.util.concurrent.ThreadPoolExecutor的包装。这个类则是spring包下的,是Spring为我们开发者提供的线程池类。
Spring提供了xml给我们配置ThreadPoolTaskExecutor线程池,但是现在普遍都在用SpringBoot开发项目,所以直接上yaml或者properties配置即可,或者也可以使用@Configuration配置也行,下面演示配置和使用:
配置:application.properties
# 核心线程池数
spring.task.execution.pool.core-size=5
# 最大线程池数
spring.task.execution.pool.max-size=10
# 任务队列的容量
spring.task.execution.pool.queue-capacity=5
# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60
# 线程池的前缀名称
spring.task.execution.thread-name-prefix=线程前缀名
配置:application.yaml
spring:
task:
execution:
pool:
#核心线程数
core-size: 5
#最大线程数
max-size: 20
#任务队列容量
queue-capacity: 10
#非核心线程存活时间
keep-alive: 60
#线程池前缀名称
thread-name-prefix: 线程前缀名
配置Config类
@Configuration
public class AsyncScheduledTaskConfig {
@Value("${spring.task.execution.pool.core-size}")
private int corePoolSize;
@Value("${spring.task.execution.pool.max-size}")
private int maxPoolSize;
@Value("${spring.task.execution.pool.queue-capacity}")
private int queueCapacity;
@Value("${spring.task.execution.thread-name-prefix}")
private String namePrefix;
@Value("${spring.task.execution.pool.keep-alive}")
private int keepAliveSeconds;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutorInit() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//最大线程数
executor.setMaxPoolSize(maxPoolSize);
//核心线程数
executor.setCorePoolSize(corePoolSize);
//任务队列的大小
executor.setQueueCapacity(queueCapacity);
//线程前缀名
executor.setThreadNamePrefix(namePrefix);
//线程存活时间
executor.setKeepAliveSeconds(keepAliveSeconds);
/**
* 拒绝处理策略
* CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
* AbortPolicy():直接抛出异常。
* DiscardPolicy():直接丢弃。
* DiscardOldestPolicy():丢弃队列中最老的任务。
*/
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
// executor.setWaitForTasksToCompleteOnShutdown(true);
//线程初始化
executor.initialize();
return executor;
}
}
线程池的使用
需在启动类加上@EnableAsync和@EnableScheduling两个注解
@Component
public class ScheduleTask {
@Qualifier("threadPoolTaskExecutorInit")
@Autowired
ThreadPoolTaskExecutor executor;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Async("threadPoolTaskExecutorInit")
@Scheduled(fixedRate = 2000)
public void test1() {
try {
Thread.sleep(6000);
System.out.println("线程池名称:" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(cron = "*/1 * * * * ?")
public void test2() throws ExecutionException, InterruptedException {
CompletableFuture<String> childThread = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+" start,time->"+System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" exit,time->"+System.currentTimeMillis());
return Thread.currentThread().getName() + "任务结束";
},executor);
System.out.println("child thread result->"+childThread.get());
}
}
4、动态控制Scheduled开关
配置yml属性开关
scheduled:
tasks:
enabled: ${tasks.enabled:true} # 本地调试通过启动参数关闭定时任务
task1State: true
task2Cron: 0 * * * * ?
代码实现
- 可以通过@Profile("test")注解来限制只有在test环境才加载当前bean
- 可以通过@ConditionalOnProperty注解来控制定时任务类的启用与否
- 可能通过动态配置cron参数,启用方式:"0 * * * * ?",禁用方式:"-" 注释掉yml属性即可
@Configuration
@EnableScheduling
// @Profile("test") // 只有在test环境才加载当前bean
@ConditionalOnProperty(prefix = "scheduled.tasks", name = "enabled", havingValue = "true")
public class SchedulingConfig {
@Value("${scheduled.tasks.task1State}")
private boolean enabled;
@Scheduled(fixedRate = 1000)
public void scheduleTask() {
if (enabled) {
// 任务逻辑,这种方式支持在服务运行时控制任务开关
}
}
@Scheduled(cron = "${scheduled.tasks.task2Cron:-}")
public void reportCurrentTime() {
// 这种方式需要重启服务才会生效
System.out.println(new Date());
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步