引言
我们平时在项目开发过程中,为了提高接口性能,经常会用到多线程异步处理,其中使用@Async
注解就是异步处理的其中一种方式。@Async
注解是Spring为提供的关键工具,它可以标记在一个方法上,使得该方法在调用时在单独的线程中执行。然而,在实际应用中,我们需要对@Async
的使用有一些深入的理解和注意事项,下面我们就一起来探索如何有效、安全地使用@Async
,并避开其中的一些常见陷阱。
@Async注解基础使用
1、启动类要加上@EnableAsync
注解,否则异步不生效
2、在任何Service或者Component类的方法上使用@Async
注解
直接上代码:
java
package com.example.springbootdemo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@Slf4j
@EnableAsync
public class SpringbootdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootdemoApplication.class, args);
log.info("project start sucess!");
}
}
java
package com.example.springbootdemo.service;
import com.example.springbootdemo.pojo.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
/**
* 查询所有用户信息
*
* @return 返回所有用户信息
*/
@RequestMapping("/findAllUser")
public List<User> findAllUser() {
long start = System.currentTimeMillis();
// 异步添加操作日志
userService.addOperationLog();
List<User> allUser = userService.findAllUser();
log.info("findUser end.time:{}", System.currentTimeMillis() - start);
return allUser;
}
}
java
package com.example.springbootdemo.service;
import com.example.springbootdemo.pojo.User;
import java.util.List;
public interface UserService {
List<User> findAllUser();
void addOperationLog();
}
java
package com.example.springbootdemo.service;
import com.example.springbootdemo.dao.UserDao;
import com.example.springbootdemo.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public List<User> findAllUser() {
return userDao.findAllUser();
}
@Override
@Async
public void addOperationLog() {
userDao.addLog();
try {
// 休眠10秒
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.springbootdemo.dao.UserDao">
<insert id="addLog">
insert into log (content, createTime) values ('查询用户信息',now())
</insert>
<!--查询所有用户-->
<select id="findAllUser" resultType="com.example.springbootdemo.pojo.User">
SELECT * FROM user
</select>
</mapper>
运行效果:
以上代码为注解正常生效的场景,接口执行时间为295毫秒,说明是异步执行了addOperationLog方法
这里列举两种非常常见容易犯错,注解不生效的场景
1、调用方和@Async能在一个类中
我们把代码改一改,把原来直接调用userService.addOperationLog()抽取到当前类中一个方法中
java
package com.example.springbootdemo.service;
import com.example.springbootdemo.pojo.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
/**
* 查询所有用户信息
*
* @return 返回所有用户信息
*/
@RequestMapping("/findAllUser")
public List<User> findAllUser() {
long start = System.currentTimeMillis();
// 异步添加操作日志
addOperationLog();
List<User> allUser = userService.findAllUser();
log.info("findUser end.time:{}", System.currentTimeMillis() - start);
return allUser;
}
@Async
public void addOperationLog() {
userService.addOperationLog();
}
}
运行效果:
接口执行了10294毫秒,findAllUser操作在addLog之后执行,说明异步不生效
原因:
- 如果在同一个类内部调用带
@Async
注解的方法,由于Spring AOP基于代理机制,默认使用的JDK动态代理或CGLIB代理只对类外部的调用起作用。所以,当同一个类内部调用时,实际上是绕过了代理,从而导致@Async
注解无效。
2、启动类没有加@EnableAsync注解
从运行结果看出,同样是不生效
@Async进阶使用
该注解是通过一个默认的线程池来异步执行方法,是spring-boot里面的TaskExecutionProperties的一个静态内部类,如果需要修改默认的配置可以在yaml或者properties中添加
默认的线程池配置:
使用springboot默认的线程池在某些场景下可能存在一些不足之处:
-
默认配置可能不适合特定业务场景:
- 默认线程池的大小、队列容量、线程存活时间等参数都是基于通用场景的预估值,可能并不适合所有的应用负载。例如,对于高并发场景,默认的线程池大小可能过小,而对于IO密集型任务,可能需要更大的队列容量或更长的线程存活时间。
-
资源浪费或不足:
- 如果默认的最大线程数过大,可能会消耗过多系统资源(如内存、CPU),特别是在服务器资源有限的情况下。相反,如果默认值过小,则可能在高峰负载期间无法充分利用系统资源处理更多任务,导致响应延迟。
-
任务积压和响应能力:
- 默认配置可能会使用无界队列,这意味着任务会一直堆积,而不考虑系统能否及时处理。在严重情况下,这可能导致内存溢出,因为队列中保存了大量待处理的任务,而且如果线程池中的线程都被占满且无法快速释放,新的请求可能会得不到及时响应。
-
异常处理和监控:
- 默认线程池可能没有配置完善的异常处理和监控机制,这对于生产环境来说是很重要的。自定义线程池可以方便地集成度量、告警和日志记录等功能。
-
事务边界问题:
- 异步方法默认不在原始事务上下文中执行,如果业务逻辑需要跨多个方法组合并保持事务一致性,就需要特殊处理。使用默认线程池时,开发者可能忽略这一点,导致数据一致性问题。
因此,建议针对具体的业务场景和技术要求,对线程池进行定制化的配置和优化,以确保系统在性能、资源利用、可扩展性等方面表现出色。
自定义线程池:
java
@Configuration
public class ThreadPoolConfig {
@Value("$(user.task.execution.pool.queueCapacity:8}")
private Integer queueCapacity;
@Value("$fuser.task.execution.pool.coresize:8)")
private Integer corePoolSize;
@Value("$(user.task.execution.pool.maxSize:8)")
private Integer maxPoolSize;
@Value("$(user.task.execution.pool.keepAlive:5)")
private Integer keepAlive;
@Value("$(user.task.execution.threadNamePrefix:user-task-]")
private String threadNamePrefix;
@Bean("userAsyncExecutor")
public Executor userAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(corePoolSize);
// 设置最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 设置队列容量
executor.setQueueCapacity(queueCapacity);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(keepAlive);
// 设置线程工厂
executor.setThreadNamePrefix(threadNamePrefix);
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
线程池使用:
运行效果:
@Async不生效汇总
-
缺少@EnableAsync注解:
- 在Spring配置类上未添加
@EnableAsync
注解,这是启用异步处理的基础。没有这个注解,Spring将不会为带有@Async
的方法创建代理,因此异步方法将同步执行。
- 在Spring配置类上未添加
-
调用方式问题(最容易踩的坑) :
- 如果在同一个类内部调用带
@Async
注解的方法,由于Spring AOP基于代理机制,默认使用的JDK动态代理或CGLIB代理只对类外部的调用起作用。所以,当同一个类内部调用时,实际上是绕过了代理,从而导致@Async
注解无效。解决办法是通过另一个类(通常是注入的服务)来调用异步方法。
- 如果在同一个类内部调用带
-
Spring Bean生命周期问题:
- 如果带有
@Async
注解的方法是在一个非Spring托管的Bean中,或者是通过new关键字直接创建的对象的方法,那么Spring AOP无法对其进行增强,因此异步注解也不会生效。
- 如果带有
-
方法签名限制:
- 被
@Async
注解的方法必须满足一定的返回类型要求,一般应该是void
或者java.util.concurrent.Future
的某种形式。若方法返回类型不符合规定,异步特性将不会生效。
- 被
-
线程池配置错误或未配置:
- 若没有正确配置线程池或者线程池未启动,虽然
@Async
注解被识别,但是任务无法投递到线程池中执行,看起来就像是异步注解未生效一样。
- 若没有正确配置线程池或者线程池未启动,虽然
-
Spring Boot环境下的自动配置问题:
- 在Spring Boot环境中,如果依赖的starter包版本不支持自动配置异步方法,或者相关配置被禁用或覆盖,也可能导致
@Async
注解不生效。
- 在Spring Boot环境中,如果依赖的starter包版本不支持自动配置异步方法,或者相关配置被禁用或覆盖,也可能导致
-
Spring Security的影响:
- 在特定情况下,Spring Security可能会干扰异步方法的正常执行,尤其是在方法级别Security配置的情况下。