1:背景
最近在項目中使用多線程對大任務拆分處理時,進行數據庫操作的時候報錯了。
業務代碼大概是這樣的:
@Service
public calss TestServiceImpl implements TestService{
@Resource
private TestMapper testMapper;
public void insert(Test test){
ThreadPoolExecutor executor = new ThreadPoolExecutor(...);
executor.execute(() -> {
testMapper.insert(test);
});
}
}
測試用例代碼:
@RunWith(SpirngRunner.class)
@SpringBootTest
public class ApplicationTests{
@Resource
private TestService testService;
@Test
public void insertTest(){
Test test = new Test();
testService.insert(test);
}
}
2:排查思路
1:項目中使用了一個很老舊的定時器工具(LTS),由於配置未接入,啟動時LTS一直會報錯的,
我首先懷疑是LTS的問題,是不是內部某部分源碼調用了DruidDataSource的close方法。
因此我在項目中先把LTS排除不讓它啟動。
結果:然並卵
2:后來懷疑是不是項目中的定時任務造成的,因為數據源關閉前打印了以下日志

可以看到 Thread-2 執行了ThreadPoolTaskExecutor 再關閉數據源的。
因此我在SpringBoot 的啟動類上加上了以下代碼
@SpringBootApplication(exclude = {TaskExecutionAutoConfiguration.class})
結果:然並卵
3:關電腦回家吃飯睡覺
第二天早上回到工位上緩了一會,思考還有什么原因會導致這種問題。
因為我們的線程池是一個封裝的工具類封裝過的(我封裝的),里面有兩行代碼引起了我的注意:

我先把 threadPoolExecutor.awaitTermination(3, TimeUnit.MINUTES); 這行代碼注釋掉,重新跑測試用例。

發現並沒有報錯,並且程序很快就終止運行了。
在這里我就想到了,SpringBoot Test 中主線程退出,是不會管用戶線程是否結束了任務的這個問題。
3:根本原因
1:SpringBoot Test 主線程退出,導致Spring 容器關閉。
2:Spring容器關閉,導致DruidDataSource 關閉
3:此時用戶線程去訪問已關閉的數據源,導致報錯。

這樣不行啊,那我測試用例怎么跑完呢?
可能這個時候有人會想到在測試用例最后面加以下代碼:
TimeUnit.SECONDS.sleep(30);
可是本人覺得不夠優雅,也不靈活。
4:解決方案
監聽容器關閉,關閉前先判斷線程池中是否存在未執行完任務的線程。
ThreadPoolExecutor 的源碼:
因此我們是可以知道是否存在未執行完任務的線程的。
所以我在項目中寫了以下代碼:
當線程池中存在未執行完任務的線程的時候,就先等待,直到所有任務完成或超過等待時間。
可把自己牛逼壞了(叉會腰)!