• SpringBoot 异步使用@Async原理及线程池配置


    所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍Spring Boot 如何实现异步任务。

    Spring中用@Async注解标记的方法,称为异步方法。在spring boot应用中使用@Async很简单:

    调用异步方法类上或者启动类加上注解@EnableAsync
    在需要被异步调用的方法外加上@Async
    所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象
    注解配置开启
    在springboot启动类(或是配置类)上添加 @EnableAsync 注解

    @EnableAsync
    public class SpringBootApplication {

    }
     
    基于@Async无返回值调用
    /**
    * 没有返回值的Async方法
    */
    @Async
    public void asyncMethodWithVoidReturnType() {
    log.info("没有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName());
    }
     
    基于@Async返回值的调用
    /**
    * 有返回值的Async方法
    * @return Future new AsyncResult
    */
    @Override
    @Async
    public Future<String> asyncMethodWithReturnType() {
    log.info("有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName());
    try {
    Thread.sleep(5000);
    return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
    log.error("出问题了, {}", e.getMessage());
    }
    return null;
    }

     
    以上示例可以发现,返回的数据类型为Future类型,其为一个接口。具体的结果类型为 AsyncResult,这个是需要注意的地方。

    调用返回结果的异步方法示例:

    @GetMapping("/future")
    public String futureAsync() throws ExecutionException, InterruptedException {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("Invoking an asynchronous method. threadName : " + Thread.currentThread().getName());

    Future<String> stringFuture = asyncService.asyncMethodWithReturnType();
    while (true) {
    if (stringFuture.isDone()) {
    stringBuilder.append("Result from asynchronous process - " + stringFuture.get());
    break;
    }
    stringBuilder.append("Continue doing something else.");
    Thread.sleep(1000);
    }
    return stringBuilder.toString();
    }

     
    分析: 这些获取异步方法的结果信息,是通过不停的检查Future的状态来获取当前的异步方法是否执行完毕来实现的。

    Spring默认线程池 SimpleAsyncTaskExecutor
    Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor,没有配置的情况下,默认使用的是 SimpleAsyncTaskExecutor。

    @Async演示Spring默认的SimpleAsyncTaskExecutor
    @Component
    @EnableAsync
    public class ScheduleTask {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Async
    @Scheduled(fixedRate = 2000)
    public void testScheduleTask() {
    try {
    Thread.sleep(6000);
    System.out.println("Spring1自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    @Async
    @Scheduled(cron = "*/2 * * * * ?")
    public void testAsyn() {
    try {
    Thread.sleep(1000);
    System.out.println("Spring2自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
    } catch (Exception ex) {
    ex.printStackTrace();
    }
    }
    }

      

    从运行结果可以看出Spring默认的@Async用线程池名字为SimpleAsyncTaskExecutor,而且每次都会重新创建一个新的线程,所以可以看到TaskExecutor-后面带的数字会一直变大。

    SimpleAsyncTaskExecutor的特点是,每次执行任务时,它会重新启动一个新的线程,并允许开发者控制并发线程的最大数量(concurrencyLimit),从而起到一定的资源节流作用。默认是concurrencyLimit取值为-1,即不启用资源节流。

    Spring的线程池 ThreadPoolTaskExecutor (自定义线程池)
    上面介绍了Spring默认的线程池simpleAsyncTaskExecutor,但是Spring更加推荐我们开发者使用ThreadPoolTaskExecutor类来创建线程池,其本质是对java.util.concurrent.ThreadPoolExecutor的包装。

    application.properties

    # 核心线程池数
    spring.task.execution.pool.core-size=5
    # 最大线程池数
    spring.task.execution.pool.max-size=10
    # 任务队列的容量
    spring.task.execution.pool.queue-capacity=5
    # 非核心线程的存活时间
    spring.task.execution.pool.keep-alive=60
    # 线程池的前缀名称
    spring.task.execution.thread-name-prefix=god-jiang-task-
     
    AsyncScheduledTaskConfig.java

    @Configuration
    public class AsyncScheduledTaskConfig {

    @Value("${spring.task.execution.pool.core-size}")
    private int corePoolSize;
    @Value("${spring.task.execution.pool.max-size}")
    private int maxPoolSize;
    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;
    @Value("${spring.task.execution.thread-name-prefix}")
    private String namePrefix;
    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;

    @Bean
    public Executor myAsync() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //最大线程数
    executor.setMaxPoolSize(maxPoolSize);
    //核心线程数
    executor.setCorePoolSize(corePoolSize);
    //任务队列的大小
    executor.setQueueCapacity(queueCapacity);
    //线程前缀名
    executor.setThreadNamePrefix(namePrefix);
    //线程存活时间
    executor.setKeepAliveSeconds(keepAliveSeconds);
    /**
    * 拒绝处理策略
    * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
    * AbortPolicy():直接抛出异常。
    * DiscardPolicy():直接丢弃。
    * DiscardOldestPolicy():丢弃队列中最老的任务。
    */
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    //线程初始化
    executor.initialize();
    return executor;
    }
    }
     
    注意,这个方法的类一定要交给Spring容器来管理

    @Component
    @EnableAsync
    public class ScheduleTask {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Async("myAsync")
    @Scheduled(fixedRate = 2000)
    public void testScheduleTask() {
    try {
    Thread.sleep(6000);
    System.out.println("Spring1自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    @Async("myAsync")
    @Scheduled(cron = "*/2 * * * * ?")
    public void testAsyn() {
    try {
    Thread.sleep(1000);
    System.out.println("Spring2自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
    } catch (Exception ex) {
    ex.printStackTrace();
    }
    }
    }
     
    以上从运行结果可以看出,自定义ThreadPoolTaskExecutor可以实现线程的复用,而且还能控制好线程数,写出更好的多线程并发程序。

    第二种自定义方式
    第一种方式的线程池使用时候总要加上注解 @Async(“myAsync”),而这种方式是重写 spring 默认线程池的方式,使用的时候只需要加 @Async 注解就可以了,不用去声明线程池类。

    NativeAsyncTaskExecutePool.java 装配线程池

    **
    * 原生(Spring)异步任务线程池装配类,实现AsyncConfigurer重写他的两个方法,这样在使用默认的
    * 线程池的时候就会使用自己重写的
    */
    @Slf4j
    @Configuration
    public class NativeAsyncTaskExecutePool implements AsyncConfigurer{

    //注入配置类
    @Autowired
    TaskThreadPoolConfig config;

    @Override
    public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //核心线程池大小
    executor.setCorePoolSize(config.getCorePoolSize());
    //最大线程数
    executor.setMaxPoolSize(config.getMaxPoolSize());
    //队列容量
    executor.setQueueCapacity(config.getQueueCapacity());
    //活跃时间
    executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
    //线程名字前缀
    executor.setThreadNamePrefix("NativeAsyncTaskExecutePool-");
    // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
    // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    // 等待所有任务结束后再关闭线程池
    executor.setWaitForTasksToCompleteOnShutdown(true);
    // 可在这初始化,也可以不初始化,在调用的时候再初始化
    executor.initialize();
    return executor;
    }

    /**
    * 异步任务中异常处理
    * @return
    */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new AsyncUncaughtExceptionHandler() {
    @Override
    public void handleUncaughtException(Throwable arg0, Method arg1, Object... arg2) {
    log.error("=========================="+arg0.getMessage()+"=======================", arg0);
    log.error("exception method:"+arg1.getName());
    }
    };
    }
    }
     
    注意点
    关于注解失效需要注意以下几点
    注解的方法必须是public方法
    方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
    异步方法使用注解@Async的返回值只能为void或者Future
    解决办法:
    如果要使同一个类中的方法之间调用也被拦截,需要使用spring容器中的实例对象,而不是使用默认的this,因为通过bean实例的调用才会被spring的aop拦截
    本例使用方法:AsyncService asyncService = context.getBean(AsyncService.class); 然后使用这个引用调用本地的方法即可达到被拦截的目的。

    为什么要使用自定义线程池
    如果不自定义异步方法的线程池默认使用SimpleAsyncTaskExecutor。SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。并发大的时候会产生严重的性能问题。

    Spring异步线程池接口 TaskExecutor
    看源码可知

    @FunctionalInterface
    public interface TaskExecutor extends Executor {
    void execute(Runnable var1);
    }
     
    它的实先类有很多如下:

     

    SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
    SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
    ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
    SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
    ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装
    拓展
    内存溢出的三种类型
    第一种OutOfMemoryError: PermGen space,发生这种问题的原因是程序中使用了大量的jar或class
    第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多
    第三种OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大
    线程池拒绝策略
    rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下

    AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
    CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
    DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
    DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

    ————————————————
    版权声明:本文为CSDN博主「我家有只小熊二」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/hundan_520520/article/details/124690284

  • 相关阅读:
    C Looooops(扩展欧几里得)题解
    POJ1061 青蛙的约会(扩展欧几里得)题解
    UVA 11426 GCD
    Trailing Zeroes (III) (二分)题解
    BZOJ 1977 次小生成树
    BZOJ 4557 侦查守卫
    codevs 1088 神经网络
    codevs 1135 选择客栈
    BZOJ 3527 力
    BZOJ 1610 连线游戏
  • 原文地址:https://www.cnblogs.com/javalinux/p/16363433.html
Copyright © 2020-2023  润新知