什么是异步任务:
异步调用和同步调用是相对的,同步调用是指程序按照预定的顺序一步一步的执行,每一步必须等待上一步完成后才能执行。
而异步调用指的是:当我们执行某个功能时,并不需要等待这个功能返回结果而是发起调用后继续执行其他操作,这个功能可以在完成后通知或者回调来告诉我们。
举个简单的栗子:比如常见的浏览器下载功能,当我们点击下载之后,浏览器会发起下载请求并执行下载功能,下载过程中我们是可以在浏览器上执行其他操作的,这就是一个典型的异步调用。多线程也是一种实现异步调用的方式。
@Async注解的使用:
开启@Async注解使用默认线程池:
- Application启动类里面使用
@EnableAsync
注解开启功能,自动扫描 - 定义异步任务类并使用
@Component
标记组件被容器扫描,异步方法加上@Async
在 Spring 中,用 @Async
注解指定的方法,该方法被调用时会以异步的方式执行。而如果没有在@Async
注解中指定线程池,就会使用默认的线程池SimpleAsyncTaskExecutor
。该线程池默认来一个任务创建一个线程,在压测情况下,会有大量请求,这时就会不断创建大量线程,极有可能出现OOM的问题。
内存溢出的三种类型:
1.OutOfMemoryError:PermGen space。程序中使用了大量的jar或class
2.OutOfMemoryError:Java heap space。java虚拟机创建的对象太多
3.OutOfMemoryError:unable to create new native thread。创建线程数量太多,占用内存过大
Spring默认线程池的参数配置解析:
在TaskExecutionProperties
与TaskExecutionAutoConfiguration
这两个类中可以查看Spring默认线程池的配置。
核心线程数:8
最大线程数:Integer.MAX_VALUE (21亿多)
阻塞队列:LinkedBlockingQueue
阻塞队列容量:Integer.MAX_VALUE
空闲线程保留时间:60s
线程池拒绝策略:AbortPolicy
其中默认核心线程数为8,在核心8个线程数占用满了之后,,新的调用就会进入阻塞队列等待执行,而创建新线程的条件是阻塞队列填满时,默认阻塞队列容量是Integer.MAX_VALUE(21亿多),阻塞队列永远不会填满, 如果有@Async注解标注的方法长期占用线程,那么新进入的调用都会进入阻塞队列等待,外部表现为没有执行,不会有日志,也不会有报错,除非OOM。
注意:@Async失效情况:
-
注解@Async的方法不是public方法
-
注解@Async的返回值只能为void或者Future
-
注解@Async方法使用static修饰也会失效
-
spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解
-
调用方与被调方不能在同一个类
- Spring 在扫描bean的时候会扫描方法上是否包含@Async注解,动态地生成一个子类(即proxy代理类),当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用
- 如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就失效了
- 所以调用方与被调方不能在同一个类,主要是使用了动态代理,同一个类的时候直接调用,不是通过生成的动态代理类调用
- 一般将要异步执行的方法单独抽取成一个类
-
类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
-
在Async 方法上标注@Transactional是没用的,但在Async 方法调用的方法上标注@Transactional 是有效的
@Async注解自定义线程池:
线程池处理异步调用的流程:
- 查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第二步。
- 查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执行第三步。
- 查看线程池是否已满,即是否达到最大线程数,不满就创建一条线程执行任务,否则就按照拒绝策略处理无法执行的任务。
配置自定义线程池:
使用Spring的默认线程池很容易导致OOM的问题,特别是在接口已经返回异步调用结果,但服务器还在执行相应的调用时,如果这个时候发生OOM,会导致数据丢失的问题。解决以上问题的办法就是自定义线程池。
- 自定义一个线程池,加入Spring IOC容器里面,即可覆盖默认线程池
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
executor.setCorePoolSize(16);
//如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
//executor.setAllowCoreThreadTimeOut(true);
//阻塞队列 当核心线程数达到最大时,新任务会放在队列中排队等待执行
executor.setQueueCapacity(124);
//最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
//任务队列已满时, 且当线程数=maxPoolSize,,线程池会拒绝处理任务而抛出异常
executor.setMaxPoolSize(64);
//当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
//允许线程空闲时间30秒,当maxPoolSize的线程在空闲时间到达的时候销毁
//如果allowCoreThreadTimeout=true,则会直到线程数量=0
executor.setKeepAliveSeconds(30);
//spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
//jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
executor.setThreadNamePrefix("自定义线程池-");
// rejection-policy:拒绝策略:当线程数已经达到maxSize的时候,如何处理新任务
// CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
// AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
// DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
// DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
- 在异步方法的@Async注解上指定线程池
@Service
@Slf4j
public class TestServiceImpl implements TestService {
@Override
@Async("threadPoolTaskExecutor")
public void testThreadPool() {
long beginTime = CommonUtil.getCurrentTimestamp();//记录任务开始时间
try {
TimeUnit.MILLISECONDS.sleep(4000);//模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = CommonUtil.getCurrentTimestamp();//记录任务结束时间
log.info("耗时={}",endTime-beginTime);
}
}
- 定义一个测试用的controller
@RestController
@RequestMapping("/api/test/v1")
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("threadPoolTest")
public JsonData sendCode() {
testService.testThreadPool();
return JsonData.buildSuccess("自定义线程池测试");
}
}
异步调用接口测试与分析:
- 用postman测试这个接口,可以看到响应时间只有7ms
- 而在控制台中可以看到,线程id从1~16之后又从1~16,因为并发量不高,所以一直循环使用的核心线程。处理时间也在4000ms左右,远大于postman中的7ms,这是因为前端返回响应后,服务器其实还在处理相应的异步调用
高并发下核心线程数配置:
- 如果是CPU密集型任务,那么线程池的线程个数应该尽量少一些,一般为CPU的个数+1条线程。
- 如果是IO密集型任务,那么线程池的线程可以放的很大,如2*CPU的个数。
- 对于混合型任务,如果可以拆分的话,通过拆分成CPU密集型和IO密集型两种来提高执行效率;如果不能拆分的的话就可以根据实际情况来调整线程池中线程的个数。