一、什么是异步编程

首先来看一下异步模型。在异步模型中,允许同一时间发生(处理)多个事件。程序调用一个耗时较长的功能(方法)时,它并不会阻塞程序的执行流程,程序会继续往下执行。当功能执行完毕时,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。

二、什么是多线程编程? 

多线程是指同时并发或并行执行多个指令(线程)。在单核处理器上,多线程往往会给人程序是在并行执行的错觉。实际上,处理器是通过调度算法在多线程之间进行切换和调度。或者根据外部输入(中断)和线程的优先级的组合来进行线程的切换。在多核处理器上,线程才是真正的并行运行。多个处理器同时执行多个线程,以达到更加高效的处理。

异步与多线程的区别?

         通过上面的介绍,我们可以看出多线程都是关于功能的并发执行。而异步编程是关于函数之间的非阻塞执行,我们可以将异步应用于单线程或多线程当中

                              因此,多线程只是异步编程的一种实现形式。

三、@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中调用了CDC/D分别使用@Transactional做了标注,则可实现事务控制的目的。)

7:异步方法使用注解@Async的返回值只能为void或者Future

8@Async需要在不同类使用才会产生异步效果,方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,如果需要从类的内部调用,需要先获取其代理类

9:没有走Spring的代理类。因为@Transactional@Async注解的实现都是基于SpringAOPAOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过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 资源,就会产生不必要的上下文切换,反而会造成整体性能的下降。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐