一、应用场景
1、同步调用
通常,在Java中的方法调用都是同步调用,比如在A方法中调用了B方法,则在A调用B方法之后,必须等待B方法执行并返回后,A方法才可以继续往下执行。
这样容易出现的一个问题就是如果B方法执行时间较长,则可能会导致调用A的请求响应迟缓 或者超时,验证影响用户体验。
为了解决这种问题,可以使用Spirng的注解@Async来用异步调用的方式处理。
2、异步调用
比如方法A调用方法B,如果B是一个异步方法,则A方法在调用B方法之后,不用等待B方法执行完成,而是直接往下继续执行别的代码。
这样,接口响应速度就会比较快。场景示例:商品库存更新接口,更新成功后,需要发送通知邮件,而接口的返回和邮件是否发送成功无关,那发送邮件这个步骤就可以写成1个异步方法进行调用。
二、含义
- 在方法上使用该@Async注解,申明该方法是一个异步任务;
- 在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;
- 使用此注解的方法的类对象,必须是spring管理下的bean对象;
- 要想使用异步任务,需要在主类上开启异步配置,即,配置上@EnableAsync注解;
三、使用步骤
1、启动类中增加@EnableAsync
以Spring boot 为例,启动类中增加@EnableAsync:
@EnableAsync // 开启异步调用
@SpringBootApplication
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、方法上加@Async注解
@Component // 添加注解表示这个类本身要被Spring管理
public class MyAsyncTask {
/**
* 异步方法
* 默认情况下,Spring 使用 SimpleAsyncTaskExecutor 去执行这些异步方法(此执行器没有限制线程数)。
* 此默认值可以从两个层级进行覆盖:
* 方法级别
* 应用级别
*/
@Async // 添加注解表示这个方法要异步执行
public void testSync() {
TimeUnit.SECONDS.sleep(10);
System.out.println("testSync");
}
}
3、测试
@Service
public class TestService {
@Autowired
private MyAsyncTask myAsyncTask;
public String sync() {
// 调用MyAsyncTask类的异步方法testSync
myAsyncTask.testSync();
return "sync";
}
}
正常情况下,执行testSync 方法会阻塞10秒。但是,可以看到,sync 方法调用 testSync 后直接立即返回,并没有等待testSync 方法阻塞10秒,这就是异步效果。
其原理是,在默认情况下开启了一个线程前缀为 task-x 的新线程在执行任务
三、异步任务的返回结果
点开@Async 注解源码,可以看到这样一句话:
In terms of target method signatures, any parameter types are supported. However, the return type is constrained to either void or java.util.concurrent.Future.
翻译:
在目标方法(说到目标方法,说到 target,说明存在一个代理对象)的签名中,入参是任何类型都支持的。但是,返回类型被限制为 void 或者 Future。
所以,异步方法的返回值只能有两种:void 或者 Future。
我们有时需要异步方法返回结果,有时不需要。
不需要返回结果的比较简单,和上面的示例一样,就不多说了。
需要接收返回结果的示例如下:
// 异步方法
@Async
public Future<Map<Long, List>> queryMap(List ids) {
List<> result = businessService.queryMap(ids);
..............
Map<Long, List> resultMap = Maps.newHashMap();
...
return new AsyncResult<>(resultMap);
}
调用异步方法的示例:
public Map<Long, List> asyncProcess(List<BindDeviceDO> bindDevices,List<BindStaffDO> bindStaffs, String dccId) {
Map<Long, List> finalMap =null;
// 返回值:
Future<Map<Long, List>> asyncResult = MyService.queryMap(ids);
try {
finalMap = asyncResult.get();
} catch (Exception e) {
...
}
return finalMap;
}
可以看到,其实返回值是可以返回任何类型的,只是说,需要用 java.util.concurrent.Future类来包装一下, 然后结果可以通过Future类的get方法获取。Future:这里V可以是任何类型。
四、自定义线程池
点开@Async注解源码,可以看到,只有1个value 属性
这个value属性就是用来传线程池的bean名称的,相当于指定线程池的意思。
上面我们提到了如果@Async不指定任何value值,那么Spring 使用默认的线程池/执行器 去执行这些异步方法,这个默认的执行器就是:SimpleAsyncTaskExecutor
这个执行器有什么特征呢?
默认核心线程数:8,
最大线程数:Integer.MAX_VALUE,
队列使用 LinkedBlockingQueue,
容量是:Integer.MAX_VALUE,
空闲线程保留时间:60s,
线程池拒绝策略:AbortPolicy
最大线程数是Integer的最大值,队列使用的是无界队列,从最大线程数和队列来看,并发情况下,这个默认线程池是有内存溢出风险的。
所以,如果业务并发量大,我们最好使用自定义线程池。
如何自定义线程池,步骤如下:
1、配置文件自定义配置参数
spring:
task:
execution:
pool:
# 最大线程数
max-size: 10
# 核心线程数
core-size: 5
# 空闲线程存活时间
keep-alive: 5s
# 队列长度
queue-capacity: 1000
# 线程名前缀
thread-name-prefix: task_name
2、编写配置类
@Configuration
@Data
public class ExecutorConfig{
/**
* 核心线程
*/
private int corePoolSize;
/**
* 最大线程
*/
private int maxPoolSize;
/**
* 队列容量
*/
private int queueCapacity;
/**
* 保持时间
*/
private int keepAliveSeconds;
/**
* 名称前缀
*/
private String preFix;
@Bean("MyExecutor")
public Executor myExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix(preFix);
executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
3、异步方法中引用
@Component
public class MyAsyncTask {
@Async("MyExecutor") //使用自定义的线程池(执行器)
public void asyncCpsItemImportTask(Long platformId, String jsonList){
//...具体业务逻辑
}
}
五、注意事项
1、无法调用同类中的@Async的方法
@Async 的原理是通过 Spring AOP 动态代理 的方式来实现的,如果a方法调用它同类中的标注@Async的b方法,是不会异步执行的,因为从a方法进入调用的都是该类对象本身,不会进入代理类。
因此,相同类中的方法调用带@Async的方法是无法异步的,这种情况仍然是同步。
@Service
public class TestService {
public String sync() {
// 调用同类中的异步方法testSync,异步无效果
testSync();
return "sync";
}
/**
* 异步方法 testSync
*/
@Async
public void testSync() {
TimeUnit.SECONDS.sleep(10);
System.out.println("testSync");
}
}
2、异步方法是static方法,@Async注解无效果
/**
* 异步方法不能是 static 方法,不然注解失效
*/
@Async
public static void test3() {
try {
log.info(Thread.currentThread().getName() + " in test3, before sleep.");
Thread.sleep(2000);
log.info(Thread.currentThread().getName() + " in test3, after sleep.");
} catch (InterruptedException e) {
log.error("sleep error.");
}
}