SpringBoot异步注解@Async解析

        在写一个绑定设备的接口,需要立即响应。但是有一个增加成长值的需求需要在这个绑定设备的接口中实现,该需求需要http调用其他项目的接口,比较耗时,同时这个需求不需要立即返回。因此,想到使用异步的方式实现该方法。于是开始研究@Async的使用,一开始就进了一个坑。

   实现异步:

  • 在启动类上添加@EnableAsync注解。
  • 在方法或类上添加@Async注解,同时在异步方法所在的类上添加@Component或@service 等注解,之后通过@Autowired使用异步类。

       1.坑一:将异步方法与调用它的方法写在了同一个类中

@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拦截,执行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.
	 * <p><b>The default is {@link AdviceMode#PROXY}.</b>
	 * 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();
        }
        //打印当前线程
        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: 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();
        }
        //打印当前线程
        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();
            }
        }
    }
}

 修正结果:

---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,创建线程数量太多,占用内存过大。

      2.坑二:@Async使用的默认线程池会导致OOM

如果使用@Async不自定义线程池,会使用默认线程池SimpleAsyncTaskExecutor。

@EnableAsync注解注释说明:

<p>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.
 <p>Supports limiting concurrent threads through the "concurrencyLimit"
 bean property. By default, the number of concurrent threads is unlimited.
 
 <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
  thread-pooling TaskExecutor implementation instead, in particular for
 executing a large number of short-lived tasks.

翻译:异步执行用户任务的SimpleAsyncTaskExecutor。每次执行客户提交给它的任务时,它会启动新的线程,并允许开发者控制并发线程的上限(concurrencyLimit),从而起到一定的资源节流作用。默认时,concurrencyLimit取值为-1,即不启用资源节流。

3.如何自定义线程池

使用默认线程池会导致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("线程池名字")使用。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章