你所不知的Spring的@Async注解

相信大家经常看到这样的文章:

当使用@Async不指定线程池时,Spring会默认使用SimpleAsyncTaskExecutor线程池,默认情况下,SimpleAsyncTaskExecutor不会限制线程创建的个数,这会导致资源耗尽。这个线程池和我们印象中的的线程池可以说是相悖的。如果需要使用SimpleAsyncTaskExecutor,则需指定线程上限(调用setConcurrencyLimit方法),避免在极端情况下出现资源耗尽的问题。另外,该任务执行器并没有执行拒绝策略,这也是在线上环境需谨慎使用的原因之一。

所以大部分人使用spring的异步都需要写如下的代码:

@Configuration
@EnableAsync
public class BaseAsyncConfigurer implements AsyncConfigurer {
 
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        //最大线程数量
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()*5);
        //线程池的队列容量
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors()*2);
        //线程名称的前缀
        executor.setThreadNamePrefix("this-excutor-");
        // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
    /*异步任务中异常处理*/
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (Throwable ex, Method method, Object... params)->{
            //todo 异步方法异常处理
            System.out.println("class#method: " + method.getDeclaringClass().getName() + "#" + method.getName());
            System.out.println("type        : " + ex.getClass().getName());
            System.out.println("exception   : " + ex.getMessage());
        };
    }
 
}

但是我很多时候并没有写如上代码,只添加了@Async直接使用(当然前提是要添加@EnableAsync注解),但是并没有发现spring每次执行的时候都创建了新的线程,而是用了线程池。于是乎怀着好奇的心情,写了如下代码,并执行多次:

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class HelloServiceImpl implements HelloService {
    @Override
    @Async
    public void hello() {
        log.info("当前线程:{}", Thread.currentThread().getName());
    }
}

执行结果如下:

2023-07-14 13:51:43.098  INFO 12268 --- [         task-1] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-1
2023-07-14 13:51:43.257  INFO 12268 --- [         task-2] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-2
2023-07-14 13:51:43.405  INFO 12268 --- [         task-3] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-3
2023-07-14 13:51:43.570  INFO 12268 --- [         task-4] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-4
2023-07-14 13:51:43.756  INFO 12268 --- [         task-5] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-5
2023-07-14 13:51:43.944  INFO 12268 --- [         task-6] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-6
2023-07-14 13:51:44.227  INFO 12268 --- [         task-7] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-7
2023-07-14 13:51:44.414  INFO 12268 --- [         task-8] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-8
2023-07-14 13:51:44.654  INFO 12268 --- [         task-1] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-1
2023-07-14 13:51:44.851  INFO 12268 --- [         task-2] c.yapler.service.impl.HelloServiceImpl   : 当前线程:task-2

这跟我想象不同,如果Spring会默认使用SimpleAsyncTaskExecutor线程池,不是应该会一直创建线程吗,这里为什么最多只会创建8个线程呢?

首先我在spring的文档中找到了TaskExecutor 的各类型描述:

  • SyncTaskExecutor: This implementation does not run invocations asynchronously. Instead, each invocation takes place in the calling thread. It is primarily used in situations where multi-threading is not necessary, such as in simple test cases.
  • SimpleAsyncTaskExecutor: This implementation does not reuse any threads. Rather, it starts up a new thread for each invocation. However, it does support a concurrency limit that blocks any invocations that are over the limit until a slot has been freed up. If you are looking for true pooling, see ThreadPoolTaskExecutor, later in this list.
  • ConcurrentTaskExecutor: This implementation is an adapter for a java.util.concurrent.Executor instance. There is an alternative (ThreadPoolTaskExecutor) that exposes the Executor configuration parameters as bean properties. There is rarely a need to use ConcurrentTaskExecutor directly. However, if the ThreadPoolTaskExecutor is not flexible enough for your needs, ConcurrentTaskExecutor is an alternative.
  • ThreadPoolTaskExecutor: This implementation is most commonly used. It exposes bean properties for configuring a java.util.concurrent.ThreadPoolExecutor and wraps it in a TaskExecutor. If you need to adapt to a different kind of java.util.concurrent.Executor, we recommend that you use a ConcurrentTaskExecutor instead.
  • WorkManagerTaskExecutor: This implementation uses a CommonJ WorkManager as its backing service provider and is the central convenience class for setting up CommonJ-based thread pool integration on WebLogic or WebSphere within a Spring application context.
  • DefaultManagedTaskExecutor: This implementation uses a JNDI-obtained ManagedExecutorService in a JSR-236 compatible runtime environment (such as a Java EE 7+ application server), replacing a CommonJ WorkManager for that purpose.

SimpleAsyncTaskExecutor描述翻译过来的意思是:该实现不重复使用任何线程。相反,它为每次调用启动一个新线程。然而,它支持并发限制,可以阻止任何超过限制的调用,直到释放出一个插槽。如果您正在寻找真正的线程池,请参阅本列表后面的 ThreadPoolTaskExecutor。
大概意思就是这个并不是严格意义上的线程池,每次调用都会启动一个新线程,但是支持并发,还是推荐我们使用ThreadPoolTaskExecutor。

我们再来看看ThreadPoolTaskExecutor的解释:该实现最常用。它公开了用于配置java.util.concurrent.ThreadPoolExecutor的Bean属性,并将其封装在TaskExecutor中。如果您需要适应不同类型的java.util.concurrent.Executor,我们建议您使用ConcurrentTaskExecutor来代替。

秉承着打破沙锅问到底的精神,我去翻阅了springboot的源码,在TaskExecutionAutoConfiguration类中,看到了如下代码:

/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.task;

import java.util.concurrent.Executor;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for {@link TaskExecutor}.
 *
 * @author Stephane Nicoll
 * @author Camille Vienot
 * @since 2.1.0
 */
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TaskExecutionProperties.class)
public class TaskExecutionAutoConfiguration {

	/**
	 * Bean name of the application {@link TaskExecutor}.
	 */
	public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

	@Bean
	@ConditionalOnMissingBean
	public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
			ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
			ObjectProvider<TaskDecorator> taskDecorator) {
		TaskExecutionProperties.Pool pool = properties.getPool();
		TaskExecutorBuilder builder = new TaskExecutorBuilder();
		builder = builder.queueCapacity(pool.getQueueCapacity());
		builder = builder.corePoolSize(pool.getCoreSize());
		builder = builder.maxPoolSize(pool.getMaxSize());
		builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
		builder = builder.keepAlive(pool.getKeepAlive());
		Shutdown shutdown = properties.getShutdown();
		builder = builder.awaitTermination(shutdown.isAwaitTermination());
		builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
		builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
		builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
		builder = builder.taskDecorator(taskDecorator.getIfUnique());
		return builder;
	}

	@Lazy
	@Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME,
			AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
	@ConditionalOnMissingBean(Executor.class)
	public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
		return builder.build();
	}

}

在applicationTaskExecutor方法中进入到了TaskExecutorBuilder类,看到了

public ThreadPoolTaskExecutor build() {
        return this.configure(new ThreadPoolTaskExecutor());
    }

发现默认使用的并不是SimpleAsyncTaskExecutor而是ThreadPoolTaskExecutor。

最后查询了各种资料才找到了原因,在spring boot2.1.0.RELEASE版本之前,默认用的确实是SimpleAsyncTaskExecutor线程池,在源码(spring boot2.0.9.RELEASE)AsyncExecutionInterceptor类中如下:

    @Nullable
    protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
        Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
        return (Executor)(defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
    }

也就是说新版本,其实spring boot默认使用的已经是ThreadPoolTaskExecutor线程池了,大家不用再去手动更改默认的线程池,不过还是可以在配置文件更改ThreadPoolTaskExecutor的参数:

#核心线程数
spring.task.execution.pool.core-size=10
#最大线程数
spring.task.execution.pool.max-size=1000
#空闲线程保留时间
spring.task.execution.pool.keep-alive=3s
#队列容量
spring.task.execution.pool.queue-capacity=1000
#线程名称前缀
spring.task.execution.thread-name-prefix=thread-prefix

具体参数根据实际情况而定。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值