Spring Boot之@Async异步线程池示例详解

目录
  • 前言
  • 一. Spring异步线程池的接口类 :TaskExecutor
  • 二、简单使用说明
  • 三、定义通用线程池
    • 1、定义线程池
    • 2、异步方法使用线程池
    • 3、通过xml配置定义线程池
  • 四、异常处理
  • 五、问题

前言

很多业务场景需要使用异步去完成,比如:发送短信通知。要完成异步操作一般有两种:

  • 1、消息队列MQ
  • 2、线程池处理。

我们来看看Spring框架中如何去使用线程池来完成异步操作,以及分析背后的原理。

一. Spring异步线程池的接口类 :TaskExecutor

在Spring4中,Spring中引入了一个新的注解@Async,这个注解让我们在使用Spring完成异步操作变得非常方便。

Spring异步线程池的接口类,其实质是java.util.concurrent.Executor

Spring 已经实现的异常线程池:

1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。

2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方

3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类

4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类

5. ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装,

关于java-多线程和线程池:https://www.jb51.net/article/222986.htm

我们查看ThreadPoolExecutor初始化的源码就知道使用ThreadPoolExecutor。

二、简单使用说明

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

1、调用异步方法类上或者启动类加上注解@EnableAsync

2、在需要被异步调用的方法外加上@Async

3、所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象;

启动类加上注解@EnableAsync:


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class CollectorApplication {

	public static void main(String[] args) throws Exception {
		SpringApplication.run(CollectorApplication.class, args);
	}
}

在需要被异步调用的方法外加上@Async,同时类AsyncService加上注解@Service或者@Component,使其对象成为Spring容器管理的bean对象;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class AsyncService {
    @Async
    public void asyncMethod(String s) {
        System.out.println("receive:" + s);
    }

    public void test() {
        System.out.println("test");
        asyncMethod();//同一个类里面调用异步方法
    }
    @Async
    public void test2() {
        AsyncService asyncService  = context.getBean(AsyncService.class);
        asyncService.asyncMethod();//异步
    }
    /**
     * 异布调用返回Future
     */
    @Async
    public Future<String> asyncInvokeReturnFuture(int i) {
        System.out.println("asyncInvokeReturnFuture, parementer="+ i);
        Future<String> future;
        try {
            Thread.sleep(1000 * 1);
            future = new AsyncResult<String>("success:" + i);
        } catch (InterruptedException e) {
            future = new AsyncResult<String>("error");
        }
        return future;
    }
}

//异步方法和普通的方法调用相同
asyncService.asyncMethod("123");
Future<String> future = asyncService.asyncInvokeReturnFuture(100);
System.out.println(future.get());

如果将一个类声明为异步类@Async,那么这个类对外暴露的方法全部成为异步方法。

@Async
@Service
public class AsyncClass {
    public AsyncClass() {
        System.out.println("----init AsyncClass----");
    }
    volatile int index = 0;
    public void foo() {
        System.out.println("asyncclass foo, index:" + index);

    }
    public void foo(int i) {
        this.index = i;
        System.out.println("asyncclass foo, index:" + i);
    }
    public void bar(int i) {
        this.index = i;
        System.out.println("asyncclass bar, index:" + i);
    }
}

这里需要注意的是:

1、同一个类里面调用异步方法不生效:原因默认类内的方法调用不会被aop拦截,即调用方和被调用方是在同一个类中,是无法产生切面的,该对象没有被Spring容器管理。即@Async方法不生效。

解决办法:如果要使同一个类中的方法之间调用也被拦截,需要使用spring容器中的实例对象,而不是使用默认的this,因为通过bean实例的调用才会被spring的aop拦截
 本例使用方法:AsyncService asyncService  = context.getBean(AsyncService.class);     然后使用这个引用调用本地的方法即可达到被拦截的目的
备注:这种方法只能拦截protected,default,public方法,private方法无法拦截。这个是spring aop的一个机制。

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

3、异步方法返回类型只能有两种:void和java.util.concurrent.Future。

1)当返回类型为void的时候,方法调用过程产生的异常不会抛到调用者层面,

可以通过注AsyncUncaughtExceptionHandler来捕获此类异常

2)当返回类型为Future的时候,方法调用过程产生的异常会抛到调用者层面

三、定义通用线程池

1、定义线程池

在Spring Boot主类中定义一个线程池,public Executor taskExecutor() 方法用于自定义自己的线程池,线程池前缀”taskExecutor-”。如果不定义,则使用系统默认的线程池。

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @EnableAsync
    @Configuration
    class TaskPoolConfig {

        @Bean
        public Executor taskExecutor1() {
            ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
            pool.setCorePoolSize(5); //线程池活跃的线程数
            pool.setMaxPoolSize(10); //线程池最大活跃的线程数
            pool.setWaitForTasksToCompleteOnShutdown(true);
            pool.setThreadNamePrefix("defaultExecutor");
            return pool;
        }

        @Bean("taskExecutor")
        public Executor taskExecutor2() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10);
            executor.setMaxPoolSize(20);
            executor.setQueueCapacity(200);
            executor.setKeepAliveSeconds(60);
            executor.setThreadNamePrefix("taskExecutor-");
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.setAwaitTerminationSeconds(60);
            return executor;
        }
    }
}

上面我们通过ThreadPoolTaskExecutor创建了一个线程池,同时设置了如下参数:

  • 核心线程数10:线程池创建时初始化的线程数
  • 最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
  • 缓冲队列200:用来缓冲执行任务的队列
  • 允许线程的空闲时间60秒:超过了核心线程数之外的线程,在空闲时间到达之后会被销毁
  • 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
  • 线程池对拒绝任务的处理策略:此处采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在execute方法的调用线程中运行被拒绝的任务;如果执行程序已被关闭,则会丢弃该任务
  • 设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
  • 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住

也可以单独类来配置线程池:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Created by huangguisu on 2020/6/10.
 */

@Configuration
@EnableAsync
public class MyThreadPoolConfig {

    private static final int CORE_POOL_SIZE = 10;

    private static final int MAX_POOL_SIZE = 20;

    private static final int QUEUE_CAPACITY = 200;

    public static final String BEAN_EXECUTOR = "bean_executor";

    /**
     * 事件和情感接口线程池执行器配置
     * @return 事件和情感接口线程池执行器bean
     *
     */
    @Bean(BEAN_EXECUTOR)
    public Executor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        // 设置队列容量
        executor.setQueueCapacity(QUEUE_CAPACITY);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("SE-Pool#Task");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

同时注意需要在配置类上添加@EnableAsync,当然也可以在启动类上添加,表示开启spring的@@Async

2、异步方法使用线程池

只需要在@Async注解中指定线程池名即可

@Component
public class Task {
    //默认使用线程池
    @Async
    public void doTaskOne() throws Exception {
        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务耗时:" + (end - start) + "毫秒");
    }
   //根据Bean Name指定特定线程池
    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务耗时:" + (end - start) + "毫秒");
    }
}

3、通过xml配置定义线程池

Bean文件配置: spring_async.xml

1. 线程的前缀为xmlExecutor

2. 启动异步线程池配置

<!-- 等价于 @EnableAsync, executor指定线程池 -->
    <task:annotation-driven executor="xmlExecutor"/>
    <!-- id指定线程池产生线程名称的前缀 -->
    <task:executor
        id="xmlExecutor"
        pool-size="5-25"
        queue-capacity="100"
        keep-alive="120"
        rejection-policy="CALLER_RUNS"/>

启动类导入xml文件:


@SpringBootApplication
@ImportResource("classpath:/async/spring_async.xml")
public class AsyncApplicationWithXML {
    private static final Logger log = LoggerFactory.getLogger(AsyncApplicationWithXML.class);

    public static void main(String[] args) {
        log.info("Start AsyncApplication.. ");
        SpringApplication.run(AsyncApplicationWithXML.class, args);
    }

}

线程池参数说明

1. ‘id' : 线程的名称的前缀

2. ‘pool-size':线程池的大小。支持范围”min-max”和固定值(此时线程池core和max sizes相同)

3. ‘queue-capacity' :排队队列长度

4. ‘rejection-policy': 对拒绝的任务处理策略

5. ‘keep-alive' : 线程保活时间(单位秒)

四、异常处理

上面也提到:在调用方法时,可能出现方法中抛出异常的情况。在异步中主要有有两种异常处理方法:

1. 对于方法返回值是Futrue的异步方法:

a) 、一种是在调用future的get时捕获异常;

b)、 在异常方法中直接捕获异常

2. 对于返回值是void的异步方法:通过AsyncUncaughtExceptionHandler处理异常

@Component
public class AsyncException {

     /**
     * 带参数的异步调用 异步方法可以传入参数
     *  对于返回值是void,异常会被AsyncUncaughtExceptionHandler处理掉
     * @param s
     */
    @Async
    public void asyncInvokeWithException(String s) {
        log.info("asyncInvokeWithParameter, parementer={}", s);
        throw new IllegalArgumentException(s);
    }

    /**
     *  异常调用返回Future
     *  对于返回值是Future,不会被AsyncUncaughtExceptionHandler处理,需要我们在方法中捕获异常并处理
     *  或者在调用方在调用Futrue.get时捕获异常进行处理
     *
     * @param i
     * @return
     */
    @Async
    public Future<String> asyncInvokeReturnFuture(int i) {
        System.out.println("asyncInvokeReturnFuture, parementer={}", i);
        Future<String> future;
        try {
            Thread.sleep(1000 * 1);
            future = new AsyncResult<String>("success:" + i);
            throw new IllegalArgumentException("a");
        } catch (InterruptedException e) {
            future = new AsyncResult<String>("error");
        } catch(IllegalArgumentException e){
            future = new AsyncResult<String>("error-IllegalArgumentException");
        }
        return future;
    }

}

实现AsyncConfigurer接口对异常线程池更加细粒度的控制

a) 创建线程自己的线程池

b) 对void方法抛出的异常处理的类AsyncUncaughtExceptionHandler

@Service
public class MyAsyncConfigurer implements AsyncConfigurer{
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
        threadPool.setCorePoolSize(1);
        threadPool.setMaxPoolSize(1);
        threadPool.setWaitForTasksToCompleteOnShutdown(true);
        threadPool.setAwaitTerminationSeconds(60 * 15);
        threadPool.setThreadNamePrefix("MyAsync-");
        threadPool.initialize();
        return threadPool;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncExceptionHandler();
    }

    /**
     * 自定义异常处理类
     */
    class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
            System.out.println("Exception message - " + throwable.getMessage());
            System.out.println("Method name - " + method.getName());
            for (Object param : obj) {
                System.out.println("Parameter value - " + param);
            }
        }

    }

}

五、问题

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

一般的错误OOM:OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大.

解决办法:一般最好使用自定义线程池,做一些特殊策略, 比如自定义拒绝策略,如果队列满了,则拒绝处理该任务。

到此这篇关于Spring Boot之@Async异步线程池的文章就介绍到这了,更多相关Spring Boot @Async异步线程池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot 如何使用@Async整合线程池

    Springboot @Async整合线程池 开篇咱们先来聊聊线程池这个概念,或者说为什么要使用线程池:简言之,充分利用cpu资源,提高程序执行时间,但是相反,线程池异常提示.主线程和子线程事务问题也是显而易见的. 那么@Async这个注解又是什么做用呢?其实就是标识方法为异步任务的一个注解,默认会自己维护一个线程池(存在弊端),利用子线程去执行任务:那么如果把这两者结合的话,线程池+Async又会有什么效果呢! 循序渐进 提到线程池,可以采用Executors提供四种线程池下,使用某些特性的场

  • springboot中@Async默认线程池导致OOM问题

    前言: 1.最近项目上在测试人员压测过程中发现了OOM问题,项目使用springboot搭建项目工程,通过查看日志中包含信息:unable to create new native thread 内存溢出的三种类型: 1.第一种OutOfMemoryError: PermGen space,发生这种问题的原意是程序中使用了大量的jar或class 2.第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多 3.第三种OutOfM

  • Spring Boot之@Async异步线程池示例详解

    目录 前言 一. Spring异步线程池的接口类 :TaskExecutor 二.简单使用说明 三.定义通用线程池 1.定义线程池 2.异步方法使用线程池 3.通过xml配置定义线程池 四.异常处理 五.问题 前言 很多业务场景需要使用异步去完成,比如:发送短信通知.要完成异步操作一般有两种: 1.消息队列MQ 2.线程池处理. 我们来看看Spring框架中如何去使用线程池来完成异步操作,以及分析背后的原理. 一. Spring异步线程池的接口类 :TaskExecutor 在Spring4中,

  • Python异步爬虫多线程与线程池示例详解

    目录 背景 异步爬虫方式 多线程,多进程(不建议) 线程池,进程池(适当使用) 单线程+异步协程(推荐) 多线程 线程池 背景 当对多个url发送请求时,只有请求完第一个url才会接着请求第二个url(requests是一个阻塞的操作),存在等待的时间,这样效率是很低的.那我们能不能在发送请求等待的时候,为其单独开启进程或者线程,继续请求下一个url,执行并行请求 异步爬虫方式 多线程,多进程(不建议) 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步会执行 弊端:不能无限制开

  • java中常见的6种线程池示例详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如下 线程池名称 描述 FixedThreadPool 核心线程数与最大线程数相同 SingleThreadExecutor 一个线程的线程池 CachedThreadPool 核心线程为0,最大线程数为Integer. MAX_VALUE ScheduledThreadPool 指定核心线程数的定时

  • Spring Boot中slf4j日志依赖关系示例详解

    前言 SpringBoot底层使用的是slf4j+logback来进行日志记录 把其他common-logging.log4j.java.util.logging转换为slf4j 下面这篇文章主要给大家介绍了关于Spring Boot slf4j日志依赖关系的相关内容,下面话不多说了,来一起看看详细的介绍吧 底层依赖关系 关系如何转化 底层通过偷梁换柱的方法,用jcl.jul.log4j中间转换包进行转化 如果要引入其他框架,必须将其中默认日志依赖剔除 SpringBoot从maven依赖中剔除

  • spring boot拦截器的使用场景示例详解

    前言 在用户登陆之后,我们一般会把用户登陆的状态和相关信息进行存储,把对应的token返回到客户端进行存储,下次请求过来时,系统可以通过token拿到当前这个用户的相关信息,这是授权通常的作法,而有时一些业务里,你存储的用户信息不是全局的,可能只是某几个接口会用户某些信息,而你把它存储起来就不是很合理:并且一些隐私信息持久化到redis也不合理,这时就需要统一对这种接口的请求做一起处理了. 拦截器HandlerInterceptor 我们可以去实现这个HandlerInterceptor接口,它

  • Spring Boot快速实现 IP地址解析的示例详解

    目录 前言 引入: 开发: 在线解析: 场景: 大家好! 我是慕歌,一只想教你学习 Spring Boot的野生coder! 欢迎来到慕歌的 Sping boot系列教程,希望通过这个教程带大家搭建基础的 Spring Boot项目,该教程所有知识点均来源于本人的真实开发! 前言 在前一节的学习中,慕歌分享了如何构建自己的小型日志用于记录一些关键性的信息,监测用户的登录状态等... 在这一节中慕歌将就上一节中关于ip 的点进行详细的讲解,带大家在spring boot 项目中获取请求的ip与详细

  • Spring Boot框架中的@Conditional注解示例详解

    目录 1. @Conditional 注解 2. Spring boot 扩展 1) @ConditionalOnClass和@ConditionalOnMissingClass注解 2) @ConditionalOnBean 和@ConditionalOnMissingBean注解 3) @ConditionalOnProperty注解 1. @Conditional 注解 @Conditional注解是Spring-context模块提供了一个注解,该注解的作用是可以根据一定的条件来使@Co

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

    目录 前言 使用步骤 配置线程池类参数配置 自定义线程任务 总结 原理刨析 文章参考 前言 在实际项目开发中很多业务场景需要使用异步去完成,比如消息通知,日志记录,等非常常用的都可以通过异步去执行,提高效率,那么在Spring框架中应该如何去使用异步呢 使用步骤 完成异步操作一般有两种,消息队列MQ,和线程池处理ThreadPoolExecutor 而在Spring4中提供的对ThreadPoolExecutor封装的线程池ThreadPoolTaskExecutor,直接使用注解启用@Asyn

  • spring boot使用自定义的线程池执行Async任务

    在前面的博客中,//www.jb51.net/article/134866.htm 我们使用了spring boot的异步操作,当时,我们使用的是默认的线程池,但是,如果我们想根据项目来定制自己的线程池了,下面就来说说,如何定制线程池! 一.增加配置属性类 package com.chhliu.springboot.async.configuration; import org.springframework.boot.context.properties.ConfigurationProper

  • Spring boot注解@Async线程池实例详解

    这篇文章主要介绍了Spring boot注解@Async线程池实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法.调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行. 1. TaskExecutor Spring异步线程池的接口类,其实质是java.util.concurrent

随机推荐