1、定时注解以及多线程
1.1、定时注解
这里首先解释定时任务@Scheduled的两个属性fixedRate和fixedDelay,
对于fixedDelay
这个注解,就是等任务结束再开始计时,例如设置fixedDelay=5000,该方法执行需要2秒,那么再次执行的时间就是2秒+5秒=7秒,即在7秒后再次执行该任务。
对于fixedRate
注解我有个误解,例如设置fixedDelay=5000,我以为每隔5秒就会调用该方法,其实不是,它根据上次任务开始的时候计时的。设置了fiexdRate=5000,该执行该方法所花的时间是2秒,那么3秒后就会再次执行该方法。
需要注意的是,如果该方法需要10分钟才执行完,5秒钟时候Spring就会再次调用这个任务,可是发现原来的任务还在执行,这个时候后续调用就阻塞了。
这里的解释来源于理解Spring定时任务@Scheduled的两个属性fixedRate和fixedDelay这篇文章。
因此这里需要增加注解来开启多线程,避免任务被阻塞:
在类上加@EnableAsync
,在方法上加@Async
注解来开启多线程模式,当到了下一次任务的执行时机时,如果之间任务还没执行完就会自动创建一个新的线程来执行它。这就符合我最初的误解了,即每5秒该任务就会由不同线程来调用一次
1.2、多线程
现在进行测试:创建定时任务,设置每10秒执行一次,但是该任务执行需要50秒
@Component
@EnableAsync
public class TestTask {
@Scheduled(fixedRate = 10000)
@Async
public void executeTask() {
// 任务内容
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"任务开始执行: " + startTime);
// 假设任务执行需要5分钟
try {
Thread.sleep(50000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("任务被中断: " + e.getMessage());
}
long endTime = System.currentTimeMillis();
System.out.println("任务执行结束: " + endTime + ", 耗时: " + (endTime - startTime) + "毫秒");
}
}
差不多每10秒就会调用一次任务,基本符合预期,
1.3 线程池
在 Spring 框架中,通过 @EnableAsync
和 @Async
注解开启多线程功能时,线程的管理是由 TaskExecutor
(任务执行器) 控制的。默认情况下,Spring 会使用 SimpleAsyncTaskExecutor
,它是一个线程池执行器,但 并不是一个真正的线程池。每次提交任务,它都会创建一个新线程,执行完后销毁线程。这就会导致资源的消耗,
比较浪费资源,这里上线程池操作:
新建线程池配置:
@Configuration
public class ScheduledPool {
@Bean(name = "taskExecutor")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 设置定时任务线程数量
taskScheduler.setPoolSize(50);
taskScheduler.setThreadNamePrefix("Custom-********");
return taskScheduler;
}
}
这里需要注意的是,Bean后面需要注明taskExecutor
,因为 Spring 自动会使用 taskExecutor(名字为 taskExecutor 的 Bean)作为全局异步执行器。如果需要多个线程池,可以在 @Async 注解中自定义 Executor 名字:
@Async("taskExecutorOne")
public void asyncMethod() {
// 你的异步逻辑
}
可以看到线程的前缀已经变成Custom-********
,说明使用的正是线程池里面的线程
这里基本应用已经完成,下面是需要注意的事项和一些原理:
2、注意事项
2.1 线程池区别
@Async
和 @Scheduled
的线程池独立性
@Scheduled
默认使用 TaskScheduler
线程池。
@Async
默认使用 TaskExecutor
线程池,
这里详细解释两个线程池的区别:
TaskScheduler
和 TaskExecutor
是 Spring 框架中两个常用的接口,用于管理线程池和异步任务处理。它们的主要区别在于设计目的、功能和使用场景。
2.2. TaskScheduler
用途: 用于处理定时任务和调度任务。
核心功能: 提供任务调度功能,可以按照固定的时间间隔或指定的时间点执行任务。
实现类:通常由 ThreadPoolTaskScheduler 实现,它结合了线程池的能力和调度功能。
使用场景:定时任务的调度,例如每隔一定时间执行任务或在某个具体时间点执行任务。
使用示例:
@Configuration
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("Scheduled-Task-");
return scheduler;
}
}
@Component
public class ScheduledTask {
@Autowired
private TaskScheduler taskScheduler;
public void startScheduledTask() {
taskScheduler.scheduleAtFixedRate(() -> {
System.out.println("Executing scheduled task at " + new Date());
}, 5000); // 每隔 5 秒执行一次
}
}
2.3 TaskExecutor
用途: 用于处理异步任务的执行。
核心功能: 是 Spring 的线程池抽象,旨在替代 java.util.concurrent.Executor,用于执行普通的并发任务。
实现类:通常由 ThreadPoolTaskExecutor 实现。还可以使用其他 Executor 实现,例如 SimpleAsyncTaskExecutor、SyncTaskExecutor 等。
使用场景:并发任务的异步执行,例如通过 @Async 注解支持的方法调用。
使用示例:
@Configuration
public class ExecutorConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("Async-Task-");
executor.initialize();
return executor;
}
}
@Component
public class AsyncTask {
@Autowired
private TaskExecutor taskExecutor;
public void executeAsyncTask() {
taskExecutor.execute(() -> {
System.out.println("Executing async task on thread: " + Thread.currentThread().getName());
});
}
}
对比
特性 | TaskScheduler | TaskScheduler |
---|---|---|
设计目的 | 定时任务调度 | 异步任务执行 |
接口继承 | 不继承 Executor | 继承 Executor |
典型实现 | ThreadPoolTaskScheduler | ThreadPoolTaskExecutor |
任务类型 | 定时任务(周期性、单次、触发器) | 普通异步任务 |
主要方法 | schedule、scheduleAtFixedRate | execute、submit |
适用场景 | 周期性任务、定时任务 | 异步任务处理,方法异步调用 |
3、异步原理以及失效情况
3.1 异步原理
@Async的原理是通过 Spring AOP 动态代理 的方式来实现的。
在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现异步执行。
所以,相同类中的方法调用带@Async的方法是无法异步的,这种情况仍然是同步。
3.2 异步失效情况情况如下:
1、 未使用@EnableAsync注解
在Spring中要开启@Async注解异步的功能,需要在项目的启动类,或者配置类上,或者调用方法的类上使用@EnableAsync注解。
2 内部方法调用
在一个方法中调用另外一个方法会失效。例如:
@Slf4j
@Service
public class UserService {
public void test() {
async("test");
}
@Async
public void async(String value) {
log.info("async:{}", value);
}
}
3 方法非public
因为private修饰的方法,只能在UserService类的对象中使用。
而@Async注解的异步功能,需要使用Spring的AOP生成UserService类的代理对象,该代理对象没法访问UserService类的private方法,因此会出现@Async注解失效的问题。
4、 方法返回值错误
想要使用@Async注解的异步功能,相关方法的返回值必须是void或者Future。
5 方法用static修饰了
使用@Async注解声明的方法,必须是能被重写的,很显然static修饰的方法,是类的静态方法,是不允许被重写的。
因此这种情况下,@Async注解的异步功能会失效。
6 方法用final修饰
在Java种final关键字,是一个非常特别的存在。
用final修饰的类,没法被继承。
用final修饰的方法,没法被重写。
用final修饰的变量,没法被修改。
如果final使用不当,也会导致@Async注解的异步功能失效
7 业务类没加@Service@controller@component等注解
业务类需要交给Spring管理,通过AOP生成代理类,因此需要加上以上注解。
在Java种final关键字,是一个非常特别的存在。
用final修饰的类,没法被继承。
用final修饰的方法,没法被重写。
用final修饰的变量,没法被修改。
如果final使用不当,也会导致@Async注解的异步功能失效
参考来源:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:Spring的异步详解(@Async)