spring异步注解@Async

1 简述

业务场景需要异步处理时,采用自建线程池,是很常见的处理方式,spring通过@Async异步注解,采用AOP编程思想,让这部分逻辑实现的更简洁、规范、完善,也让系统更容易维护,以下是该部分知识点、常见问题整理。

通常进行异步处理时,从业务场景角度,大致分“实时”、“非实时”两种业务场景。

非实时:异步任务不急于马上处理,在业务允许的时间范围内处理就行,像日志、通知、消息等等,这种业务场景也常常采用MQ处理。从概念上说跟“异步”更贴合。

实时:任务必须马上处理,对时间要求比较高,不惜把任务分解为多个小任务,异步并行处理,来加快执行速度,像响应时间要求比较高的API接口等。从概念上说跟“并行”更贴合。

可以看出,不同类型的业务场景,对线程池的要求是不一样的,需要根据实际情况,合理的选择、配置线程池。

2 配置

2.1 开启

开启异步功能,需添加启动注解@EnableAsync,否则异步注解@Async不起作用。

2.2 线程池

采用异步方式处理,对线程的管理,就离不开线程池,以下是线程池的几种配置方式。

(1) 不配置(默认)

如果不做任何配置,spring-boot 会默认自动构建一个ThreadPoolTaskExecutor线程池类bean, 来管理这些执行异步方法的线程,这个默认线程池的具体参数值,可参考TaskExecutionProperties类定义的默认值,如下:

// pool
private int queueCapacity = Integer.MAX_VALUE;
private int coreSize = 8;
private int maxSize = Integer.MAX_VALUE;
private boolean allowCoreThreadTimeout = true;
private Duration keepAlive = Duration.ofSeconds(60);

// thread
private String threadNamePrefix = "task-";

可以看出,默认线程池,采用无限容量队列,这也就限制了它的最大线程数不会超过8个,通常情况下,需要根据业务场景重新调整这些参数。


(2) spring配置

spring-boot项目中TaskExecutionAutoConfiguration类,由它自动加载线程池配置参数,并构建ThreadPoolTaskExecutor线程池类bean,以下是约定的配置项:

spring:
  task:
    execution:
      pool:
        core-size: 8 
        max-size: 100
        queue-capacity: 0
        allow-core-thread-timeout: false
        keep-alive: 60s
      thread-name-prefix: my-task-


(3) java代码配置

@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {

    private static final String THREAD_NAME_PREFIX = "my-async-task-";
    
    @Bean("myTaskExecutor")
    @Primary
    public TaskExecutor getAsyncExecutor() {
    	ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    	
        // 核心线程数
        executor.setCorePoolSize(8);
        // 最大线程数
        executor.setMaxPoolSize(20);
        // 队列容量
        executor.setQueueCapacity(0);
        // 线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 线程名称前缀
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        // 允许核心线程空闲超时
        executor.setAllowCoreThreadTimeOut(false);
        
        // 拒绝策略(抛异常)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 拒绝策略(由创建任务的线程处理)
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        return executor;
    }
    
    /*
     * 捕获异步未处理的异常
     */
	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return new MySimpleAsyncUncaughtExceptionHandler();
	} 
	
	private static class MySimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
		private static final Logger logger = LoggerFactory.getLogger(MySimpleAsyncUncaughtExceptionHandler.class);

		@Override
		public void handleUncaughtException(Throwable ex, Method method, Object... params) {
			if (logger.isErrorEnabled()) {
				logger.error("捕获async method未处理异常 : " + method, ex);
			}
		}
	}	
}

这种方式比较灵活,可以调整拒绝策略、线程工厂等参数,还可以对未处理异常捕获处理。

(4) 多线程池

采用"(3) java代码配置"方式,按同样的方式再添加TaskExecutor类型bean,重新命名bean-name 就可以,然后通过@Async("线程池bean-name")的参数,来匹配对应的线程池bean,就可以对异步方法进行线程池隔离处理了。

3 异步

3.1 注解@Async

(1) @Async注解既可以方法标记,也可以类标记,如果在类上,则该类的所有方法都会进行异步处理。
(2) 方法定义
对方法的形参没有限制,可以是任何类型,但返回值类型,仅支持是 void、Future、ListenableFuture、CompletableFuture类型,如果定义为之外的其他类型,则调用方只能获取到null值。另外,如果方法有返回值,需要用AsyncResult类对返回值包装一下。

(3) 如果有多个线程池, 可以通过@Async("线程池bean-name"),来匹配指定的线程池。

3.2 例子

(1) 异步方法类

@Service
//@Async("myTaskExecutor")
public class MyAsyncService {
	
	private static final Logger logger = LoggerFactory.getLogger(MyAsyncService.class);
	
	public void syncMethod(String value) throws Exception {
		logger.info("syncMethod running: " + Thread.currentThread().getName());
		this.method0(value);
		logger.info("syncMethod running: " + Thread.currentThread().getName());
	}
	
	@Async
	public String errorMethod0(String value) throws InterruptedException {		
    	logger.info("errorMethod0 running: " + Thread.currentThread().getName());
        Thread.sleep(1000 * 2);
        return "调用者,获取到null";
	}
	
	@Async
	public void method0(String value) throws InterruptedException {		
    	logger.info("method0 running: " + Thread.currentThread().getName());
        Thread.sleep(1000 * 2);
	}

	@Async
    public Future<MyResult> method1(String value) throws InterruptedException {
    	logger.info("method1 running: " + Thread.currentThread().getName());
        Thread.sleep(1000 * 5);        
        return new AsyncResult<MyResult>(new MyResult(value));        
    }
    
	@Async
    public Future<MyResult> method2(String value) throws InterruptedException {
    	logger.info("method2 running: " + Thread.currentThread().getName());
        Thread.sleep(1000 * 10);    	        
        return new AsyncResult<MyResult>(new MyResult(value));
    }
    
	@Async
    public ListenableFuture<MyResult> method3(String value) throws InterruptedException {
    	logger.info("method3 running: " + Thread.currentThread().getName());
        Thread.sleep(1000 * 10);    	        
        return new AsyncResult<MyResult>(new MyResult(value));
    }
}

(2) 调用

@Service
public class TestMyAsyncService {
	
	private static final Logger logger = LoggerFactory.getLogger(TestMyAsyncService.class);

    @Autowired
    private MyAsyncService asyncService;
    
    /*
     * 同步处理
     */
    public void syncProcess() throws Exception {
    	asyncService.syncMethod("hello");
    	logger.info("hi, syncProcess");
    }
    
    /*
     * 错误处理
     */
    public void errorProcess() throws Exception {
    	String result = asyncService.errorMethod0("hello");
    	logger.info("hi, errorProcess, result: {}", result);
    }

    /*
     * 异步处理
     */
    public void asyncProcess() throws Exception {
    	asyncService.method0("hello");
    	logger.info("hi, asyncProcess");
    }

    /*
     * 并行处理
     */
    public void parallelProcess() throws Exception {

        Future<MyResult> result1 = asyncService.method1("hello");
        Future<MyResult> result2 = asyncService.method2("async");

        String result = String.format("%s, %s", result1.get(), result2.get());
    	logger.info("hi, parallelProcess, result: {}", result);
    }
    
    /*
     * 异步回调
     */
    public void asyncCallback() throws Exception {
    	
    	logger.info("asyncCallback running: " + Thread.currentThread().getName());
    	ListenableFuture<MyResult> result1 = asyncService.method3("hello");
    	
    	Thread.sleep(1000);
    	
    	result1.addCallback(new SuccessCallback<MyResult>() {
			@Override
			public void onSuccess(MyResult result) {
				logger.info("asyncCallback running onSuccess:{}, {}", result, Thread.currentThread().getName());				
			}
    	}, new FailureCallback() {
			@Override
			public void onFailure(Throwable ex) {
				logger.error("asyncCallback running onFailure" + Thread.currentThread().getName(), ex);				
			}
    	});
    	
    	logger.info("hi, asyncCallback");
    }
}

上面给出了同步、异步、并行、回调处理的简单例子,其中ListenableFuture类型返回值,可以通过增加SuccessCallback,FailureCallbac方法,以异步非阻塞方式处理结果,而不是像Future类型要阻塞当前线程等待结果。

思索一下,对于ListenableFuture类型返回值,是不是有一个疑点,如果异步方法执行的足够快,在添加SuccessCallback,FailureCallbac方法时,异步方法已经执行完毕,那这种情况SuccessCallback,FailureCallback方法还会被调用吗?

多虑了,spring贵为基础类库,不会出现这种低级漏洞,增加的callback方法仍会被调用,但会以不同方式处理,通常情况下callback方法由执行任务的异步线程来调用,而刚才我们的疑惑是,如果addCallback时,异步线程已执行结束,那callback方法由谁调用呢?这是由于在addCallback时,会检查异步线程状态,如果已结束,则会在当前线程直接执行这些方法。

4 异常

执行异步任务时,抛出的异常,必须进行捕获处理,避免抛出异常被吞掉,系统却没有任何感知的糟糕情况。

异步方法的异常捕获,分两种情况:

第一种: 返回值为void类型

这种方法,在异步调用后,基本上就跟当前线程无关了,spring提供了默认的异常捕获SimpleAsyncUncaughtExceptionHandler类,对异常做了简单的日志记录。
如果需要自己捕获,建议通过扩展spring提供的AsyncConfigurerSupport类来处理,重新实现getAsyncUncaughtExceptionHandler方法就可以,上面"2.2 线程池(3) java代码配置"部分AsyncConfig类,已提供demo实现。

第二种: 返回值为非void类型(也就是Future系列,不讨论那些不规范的返回值类型)

这种类型的异步方法,不需要另外的异常捕获配置,调用Future类的get()方法时,在当前线程捕获处理就可以了。

看似完美,但坑来了, 此种类型的方法,必须调用get() 或 addCallback(),否则异常就会被吞掉,导致系统感知不到这些异常。

5 常见问题

(1) 根据业务场景的异步、并行特点,来选择、配置合适的线程池。

(2) 完善异常捕获,避免异常被吞掉,让系统感知不到问题。

(3) 系统有多个线程池bean时,最好在异步方法检测一下运行的线程,确认是否已异步、是否是所匹配的线程池。

(4) 线程池配置缓存队列,会出现任务丢失问题,需要根据实际业务场景权衡处理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值