在寫一個綁定設備的接口,需要立即響應。但是有一個增加成長值的需求需要在這個綁定設備的接口中實現,該需求需要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("線程池名字")使用。