一、什么是异步编程
首先来看一下异步模型。在异步模型中,允许同一时间发生(处理)多个事件。程序调用一个耗时较长的功能(方法)时,它并不会阻塞程序的执行流程,程序会继续往下执行。当功能执行完毕时,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。
二、什么是多线程编程?
多线程是指同时并发或并行执行多个指令(线程)。在单核处理器上,多线程往往会给人程序是在并行执行的错觉。实际上,处理器是通过调度算法在多线程之间进行切换和调度。或者根据外部输入(中断)和线程的优先级的组合来进行线程的切换。在多核处理器上,线程才是真正的并行运行。多个处理器同时执行多个线程,以达到更加高效的处理。
异步与多线程的区别?
通过上面的介绍,我们可以看出多线程都是关于功能的并发执行。而异步编程是关于函数之间的非阻塞执行,我们可以将异步应用于单线程或多线程当中
因此,多线程只是异步编程的一种实现形式。
三、@Async注解实现异步:
在启动类上添加@EnableAsync注解。在方法或类上添加@Async注解,同时在异步方法所在的类上添加@Component或@service 等注解,之后通过@Autowired使用异步类。
四、Springboot中使用@Async实现异步处理的注意事项
使用了@Async ,却没有实现异步的情况:
1:异步方法使用static修饰,必须是public方法
2:异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
3:异步方法不能与异步方法在同一个类中
4:类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
5:如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解
6:在Async 方法上标注@Transactional是没用的。 在Async 方法调用的方法上标注@Transactional 有效。(例如: 方法A,使用了@Async/@Transactional来标注,但是无法产生事务控制的目的。方法B,使用了@Async来标注, B中调用了C、D,C/D分别使用@Transactional做了标注,则可实现事务控制的目的。)
7:异步方法使用注解@Async的返回值只能为void或者Future
8:@Async需要在不同类使用才会产生异步效果,方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,如果需要从类的内部调用,需要先获取其代理类
9:没有走Spring的代理类。因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器管理
问题:springboot中@Async默认线程池易导致OOM
解决办法:使用自定义线程池。
问:为什么springboot中@Async默认线程池易导致OOM问题?或者至少存在OOM风险?
答:我们平常如果想使用spring自带的线程池,可以使用@EnableAsync,然后在需要异步调用的方法上加上@Async即可异步调用。但是spring调用异步方法的默认的线程池SimpleAsyncTaskExecutor却并不是真正意义上的线程池,它会为每一个任务都创建一个线程,这样当我们一次性有很多的任务来时,就会创建大量的线程,可能造成OOM.
自定义线程池demo如下:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
@Bean("MyPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
executor.setCorePoolSize(8);
//如果设置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("MyPoolTaskExecutor")注解,该方法变成线程池处理的异步方法。
自定义线程池的注意事项:
对于线程池大小的设定,我们需要考虑的问题有:
1 CPU个数
2 内存大小
3 任务类型,是计算密集型(CPU密集型)还是I/O密集型
4 是否需要一些稀缺资源,像数据库连接这种等等
CPU密集型
第一种是 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。
最佳线程数 = CPU 核心数的 1~2 倍
如果设置过多的线程,实际上并不会起到很好的效果。此时假设我们设置的线程数是 CPU 核心数的 2 倍以上, 因为计算机的任务很重,会占用大量的 CPU 资源,所以这是 CPU 每个核心都是满负荷工作,而设置过多的线程数,每个线程都去抢占 CPU 资源,就会产生不必要的上下文切换,反而会造成整体性能的下降。