多線程下嵌套異步任務(wù)導(dǎo)致程序假死問題
問題描述
線上環(huán)境異步任務(wù)全部未執(zhí)行,代碼沒有拋出任何異常和提示,CPU、內(nèi)存都很正常,基本沒有波動(dòng),GC也沒啥異常的。
問題原因
經(jīng)定位是異步由于嵌套異步任務(wù)使用了Future.get()方法導(dǎo)致的程序阻塞
手動(dòng)使用線程池示例
public class FutureBlockTest {
public static void main(String[] args) {
// 為了模擬我這里只存創(chuàng)建一個(gè)工作線程
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
// 第一層異步任務(wù)
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "-main-thread");
// 第二層異步任務(wù)(嵌套任務(wù))
FutureTask<Long> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "-child-thread");
return 10L;
});
fixedThreadPool.execute(futureTask);
System.out.println("子任務(wù)提交完畢");
// 獲取子線程的返回值
try {
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
};
// 提交主線
fixedThreadPool.submit(runnable);
}
}
執(zhí)行上訴示例后輸出
pool-1-thread-1-main-thread
子任務(wù)提交完畢
然后程序假死。
使用@Async示例
// 程序入口
@Controller
public class AsyncController {
@Autowired
private MainThreadService mainThreadService;
@GetMapping("/")
public String helloWorld() throws Exception {
mainThreadService.asyncMethod();
return "Hello World";
}
}
// 主任務(wù)代碼
@Service
public class MainThreadService {
@Autowired
private ChildThreadService childThreadService;
@Async("asyncThreadPool")
public void asyncMethod() throws Exception {
// 主任務(wù)開始
// TODO
// 開啟子任務(wù)
Future<Long> longFuture = childThreadService.asyncMethod();
// 子任務(wù)阻塞子任務(wù)
longFuture.get();
// TODO
}
}
// 子任務(wù)示例
@Service
public class ChildThreadService {
@Async("asyncThreadPool")
public Future<Long> asyncMethod() throws Exception {
// 子任務(wù)執(zhí)行
Thread.sleep(1000);
// 返回異步結(jié)果
return new AsyncResult<>(10L);
}
}定位
1.通過jps和 jstack命令定位
jstack 81173 | grep 'WAITING' -A 15
admin@wangyuhao spring-boot-student % jstack 81173 | grep 'WAITING' -A 15
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076b541b38> (a java.util.concurrent.FutureTask)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
at java.util.concurrent.FutureTask.get(FutureTask.java:191)
at com.xiaolyuh.FutureBlockTest.lambda$main$1(FutureBlockTest.java:28)
at com.xiaolyuh.FutureBlockTest$$Lambda$1/885951223.run(Unknown Source)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
可以定位到是futureTask.get()發(fā)生了阻塞。
2.也可以使用 Arthas定位


| 狀態(tài) | 場(chǎng)景 | 原因 |
|---|---|---|
| BLOCKED | 線程處于BLOCKED狀態(tài)的場(chǎng)景 | 1.當(dāng)前線程在等待一個(gè)monitor lock,比如synchronizedhuo或者Lock。 |
| WAITING | 線程處于WAITING狀態(tài)的場(chǎng)景 | 1. 調(diào)用Object對(duì)象的wait方法,但沒有指定超時(shí)值。 2. 調(diào)用Thread對(duì)象的join方法,但沒有指定超時(shí)值。 3. 調(diào)用LockSupport對(duì)象的park方法。 |
| TIMED_WAITING | 線程處于TIMED_WAITING狀態(tài)的場(chǎng)景 | 1. 調(diào)用Thread.sleep方法。 2. 調(diào)用Object對(duì)象的wait方法,指定超時(shí)值。 3. 調(diào)用Thread對(duì)象的join方法,指定超時(shí)值。 4. 調(diào)用LockSupport對(duì)象的parkNanos方法。 5. 調(diào)用LockSupport對(duì)象的parkUntil方法。 |
問題分析
線程池內(nèi)部結(jié)構(gòu)

當(dāng)線程1中的任務(wù)A嵌套了任務(wù)C后,任務(wù)C被放到了阻塞隊(duì)列,這時(shí)線程1就被柱塞了,必須等到任務(wù)C執(zhí)行完畢。
這時(shí)如果其他線程也發(fā)生相同清空,如線程2的任務(wù)B,他的嵌套任務(wù)D也被放入阻塞隊(duì)列,這是線程2也會(huì)被阻塞。
如果這類任務(wù)比較多時(shí)就會(huì)將所有線程池的線程阻塞住。最后導(dǎo)致線程池假死,所有異步任務(wù)無法執(zhí)行。
解決辦法
- futureTask.get()必須加上超時(shí)時(shí)間,這樣至少不會(huì)導(dǎo)致程序一直假死
- 不要使用嵌套的異步任務(wù),或者嵌套任務(wù)不要獲取子任務(wù)結(jié)果,不要阻塞主任務(wù)
- 將主任務(wù)和子任務(wù)的線程池拆分成兩個(gè)線程池池,不要使用同一個(gè)線程池(推薦)
思考
我們程序代碼使用的@Async注解,也就是示例二的代碼。使用注解默認(rèn)配置,那么Spring會(huì)給所有任務(wù)分配單獨(dú)線程,且線程不能重用,源碼如下:
獲取Executor源碼
org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor
/**
* This implementation searches for a unique {@link org.springframework.core.task.TaskExecutor}
* bean in the context, or for an {@link Executor} bean named "taskExecutor" otherwise.
* If neither of the two is resolvable (e.g. if no {@code BeanFactory} was configured at all),
* this implementation falls back to a newly created {@link SimpleAsyncTaskExecutor} instance
* for local use if no default could be found.
* @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME
*/
@Override
protected Executor getDefaultExecutor(BeanFactory beanFactory) {
Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}獲取執(zhí)行任務(wù)源碼
org.springframework.core.task.SimpleAsyncTaskExecutor#doExecute
/**
* Template method for the actual execution of a task.
* <p>The default implementation creates a new Thread and starts it.
* @param task the Runnable to execute
* @see #setThreadFactory
* @see #createThread
* @see java.lang.Thread#start()
*/
protected void doExecute(Runnable task) {
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
thread.start();
}我們可以發(fā)現(xiàn)默認(rèn)執(zhí)行@Async注解的異步線程池,內(nèi)部其實(shí)就沒用線程池,它會(huì)給每一個(gè)任務(wù)創(chuàng)建一個(gè)新的線程,線程使用過后會(huì)銷毀掉,線程不會(huì)重用。
- 那它將會(huì)帶來一個(gè)問題,那就是異步任務(wù)過多就會(huì)不斷創(chuàng)建線程,最終將系統(tǒng)資源耗盡。
- 這也是網(wǎng)絡(luò)上大部分文章不推薦直接使用@Async注解默認(rèn)配置的原因。
我們需要思考的是,Spring的設(shè)計(jì)這為什么要這樣設(shè)計(jì),這里有這么明顯的問題,難道他們不知道嗎,我理解這樣設(shè)計(jì)的初衷可能就是為了避免上訴我們發(fā)現(xiàn)的任務(wù)嵌套問題,因?yàn)槊總€(gè)任務(wù)單獨(dú)線程執(zhí)行是不會(huì)發(fā)生上訴程序假死的情況的。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
mybatis3.4.6 批量更新 foreach 遍歷map 的正確姿勢(shì)詳解
這篇文章主要介紹了mybatis3.4.6 批量更新 foreach 遍歷map 的正確姿勢(shì)詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
mybatis Map查詢結(jié)果下劃線轉(zhuǎn)駝峰的實(shí)例
這篇文章主要介紹了mybatis Map查詢結(jié)果下劃線轉(zhuǎn)駝峰的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
SpringBoot2.x實(shí)現(xiàn)給Controller的RequestMapping添加統(tǒng)一前綴
這篇文章主要介紹了SpringBoot2.x實(shí)現(xiàn)給Controller的RequestMapping添加統(tǒng)一前綴,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
使用Feign調(diào)用時(shí)添加驗(yàn)證信息token到請(qǐng)求頭方式
這篇文章主要介紹了使用Feign調(diào)用時(shí)添加驗(yàn)證信息token到請(qǐng)求頭方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringBoot集成ZXing實(shí)現(xiàn)二維碼的生成與讀取功能
本教程將詳細(xì)講解如何在 Spring Boot 項(xiàng)目中集成 ZXing 庫實(shí)現(xiàn)二維碼的生成(返回 Base64 編碼)和讀?。ń馕鰣D片的二維碼)功能,并覆蓋常見異常處理、參數(shù)優(yōu)化等實(shí)戰(zhàn)要點(diǎn),適合 Java 開發(fā)新手快速上手,需要的朋友可以參考下2026-03-03
IntelliJ?IDEA無公網(wǎng)遠(yuǎn)程Linux服務(wù)器環(huán)境開發(fā)過程(推薦收藏)
下面介紹如何在IDEA中設(shè)置遠(yuǎn)程連接服務(wù)器開發(fā)環(huán)境并結(jié)合Cpolar內(nèi)網(wǎng)穿透工具實(shí)現(xiàn)無公網(wǎng)遠(yuǎn)程連接,然后實(shí)現(xiàn)遠(yuǎn)程Linux環(huán)境進(jìn)行開發(fā),感興趣的朋友跟隨小編一起看看吧2023-12-12
SpringBoot之@Controller和@RequestMapping的實(shí)現(xiàn)原理解讀
這篇文章主要介紹了SpringBoot之@Controller和@RequestMapping的實(shí)現(xiàn)原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04

