在写一个绑定设备的接口,需要立即响应。但是有一个增加成长值的需求需要在这个绑定设备的接口中实现,该需求需要http调用其他项目的接口,比较耗时,同时这个需求不需要立即返回。因此,想到使用异步的方式实现该方法。于是开始研究@Async的使用,一开始就进了一个坑。
实现异步:
@Component
public class AsyncDemo {
public void test() {
//异步方法
for (int i = 0; i < 10; i++) {
print();
}
//打印当前线程
for (int i = 0; i < 10; i++) {
System.out.println("---main: thread name: " + Thread.currentThread().getName() );
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Async
public void print() {
System.out.println("--- async: thread name: " + Thread.currentThread().getName() );
}
}
测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
public class AsyncTest {
@Autowired
private AsyncDemo asyncDemo;
@Test
public void test(){
asyncDemo.test();
}
}
结果输出:
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
发现异步方法变成了同步执行。于是查找源码:@EnableAsync注解有个默认模式是AdviceMode.PROXY。该模式会走代理。
具体代码这里不在追述,详细请看:异步任务spring @Async注解源码解析
简述一下:spring 在扫描bean的时候会扫描方法上是否包含@Async注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就没有增加异步作用,我们看到的现象就是@Async注解无效,调用它的线程也是主线程。
伪代码:
@Service
class A{
@Async
method b(){...}
method a(){ //标记1
b();
}
}
//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法加上异步效果
class proxy$A{
A objectA = new A();
method b(){ //标记2
//异步执行Async
objectA.b();
}
method a(){ //标记3
objectA.a(); //由于a()没有注解,所以不会异步执行,而是直接调用A的实例的a()方法
}
}
当我们调用A的bean的a()方法的时候,也是被proxyA拦截,执行proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以异步执行的效果也没有运行。
以上解释:转自https://blog.csdn.net/clementad/article/details/47339519
参考:https://stackoverflow.com/questions/18590170/transactional-does-not-work-on-method-level
查看源码:
启动类的注解@EnableAsync中一段代码:
/** * Indicate how async advice should be applied. *
The default is {@link AdviceMode#PROXY}. * Please note that proxy mode allows for interception of calls through the proxy * only. Local calls within the same class cannot get intercepted that way; an * {@link Async} annotation on such a method within a local call will be ignored * since Spring's interceptor does not even kick in for such a runtime scenario. * For a more advanced mode of interception, consider switching this to * {@link AdviceMode#ASPECTJ}. */ AdviceMode mode() default AdviceMode.PROXY;
即:#model属性控制应用程序的通知方式,模型默认是AdviceMode PROXY,由其他属性控制代理的行为。请注意,代理模式能拦截的调用仅限于通过代理进行的调用,并且不能以这种方式拦截同一类中的本地调用。
如果不通过@Autowired方式调用异步方法,而是通过new一个异步方法所在的类的方式调用,@Async也会失效,因为这也是本地调用。
例如:
异步方法所在的类:
@Component
public class Print {
@Async
public void print(){
System.out.println("--- async: thread name: " + Thread.currentThread().getName());
}
}
调用:
@Component
public class AsyncDemo {
public void test() {
//异步方法
Print print = new Print();
for (int i = 0; i < 10; i++) {
print.print();
//打印当前线程
System.out.println("---main: thread name: " + Thread.currentThread().getName());
}
}
}
结果:
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
--- async: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
修正:
将异步方法写在另一个类中。
@Component
public class AsyncDemo {
@Autowired
private Print print;
public void test() {
//异步方法
for (int i = 0; i < 10; i++) {
print.print();
//打印当前线程
System.out.println("---main: thread name: " + Thread.currentThread().getName());
}
}
}
修正结果:
---main: thread name: main
--- async: thread name: SimpleAsyncTaskExecutor-3
--- async: thread name: SimpleAsyncTaskExecutor-4
--- async: thread name: SimpleAsyncTaskExecutor-5
--- async: thread name: SimpleAsyncTaskExecutor-7
--- async: thread name: SimpleAsyncTaskExecutor-2
--- async: thread name: SimpleAsyncTaskExecutor-8
--- async: thread name: SimpleAsyncTaskExecutor-1
--- async: thread name: SimpleAsyncTaskExecutor-9
--- async: thread name: SimpleAsyncTaskExecutor-10
--- async: thread name: SimpleAsyncTaskExecutor-6
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
可以看出异步方法起作用了。但这里有个问题,调用了10次异步方法,发现开了10个线程。如果异步方法被调用很多次,岂不是要创建很大线程,导致OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大。
如果使用@Async不自定义线程池,会使用默认线程池SimpleAsyncTaskExecutor。
@EnableAsync注解注释说明:
By default, Spring will be searching for an associated thread pool definition: either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context, or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} will be used to process async method invocations. Besides, annotated methods having a {@code void} return type cannot transmit any exception back to the caller. By default, such uncaught exceptions are only logged.
翻译一下:默认情况下,spring会先搜索TaskExecutor类型的bean或者名字为taskExecutor的Executor类型的bean,如果都不存在,使用SimpleAsyncTaskExecutor执行器。
我们来看一下这个SimpleAsyncTaskExecutor类的注解说明:
{@link TaskExecutor} implementation that fires up a new Thread for each task, executing it asynchronously.
Supports limiting concurrent threads through the "concurrencyLimit" bean property. By default, the number of concurrent threads is unlimited.
NOTE: This implementation does not reuse threads! Consider a thread-pooling TaskExecutor implementation instead, in particular for executing a large number of short-lived tasks.
翻译:异步执行用户任务的SimpleAsyncTaskExecutor。每次执行客户提交给它的任务时,它会启动新的线程,并允许开发者控制并发线程的上限(concurrencyLimit),从而起到一定的资源节流作用。默认时,concurrencyLimit取值为-1,即不启用资源节流,来多少个任务就创建多少个线程。
使用默认线程池会导致OOM,那么我们如何自定义线程池呢。
查看官方文档:
实现:
1.配置文件:在spingboot的properties中配置
spring.task.execution.pool.core-threads = 3
spring.task.execution.pool.max-threads = 5
spring.task.execution.pool.queue-capacity = 100
spring.task.execution.pool.keep-alive = 10
2.写配置类
@Configuration
public class ThreadsConfig implements AsyncConfigurer {
@Value("${spring.task.execution.pool.core-threads}")
private int corePoolSize;
@Value("${spring.task.execution.pool.max-threads}")
private int maxPoolSize;
@Value("${spring.task.execution.pool.queue-capacity}")
private int queueCapacity;
@Value("${spring.task.execution.pool.keep-alive}")
private int keepAliveSeconds;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
//设置核心线程数
threadPool.setCorePoolSize(corePoolSize);
//设置最大线程数
threadPool.setMaxPoolSize(maxPoolSize);
//线程池所使用的缓冲队列
threadPool.setQueueCapacity(queueCapacity);
//等待任务在关机时完成--表明等待所有线程执行完
threadPool.setWaitForTasksToCompleteOnShutdown(true);
//线程池的工作线程空闲后(指大于核心又小于max的那部分线程),保持存活的时间
threadPool.setAwaitTerminationSeconds(keepAliveSeconds);
//饱和策略默认是直接抛弃任务
//初始化线程
threadPool.initialize();
return threadPool;
}
}
配置完成后@Async注解就会使用该线程池,有需要的话可以在getAsyncExecutor()方法添加@Bean("线程池名字")注解指定线程池名字,最后在@Async("线程池名字")使用。