在上一篇博客中,我使用了線程池進行管理線程,達到線程複用的效果。
詳情參考:線程池+CountDownLatch優化代碼,提高程序執行效率
程序啓動、運行皆無異常,線程池確實對程序中創建的線程進行管理,但是,在我關閉tomcat時,無法正常關閉,程序出現報錯。報錯信息如下:
05-Apr-2020 19:09:45.003 璀﹀憡 [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads
The web application [ROOT] appears to have started a thread named [pool-5-thread-2] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:492)
java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:680)
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java.lang.Thread.run(Thread.java:748
注意看這行報錯信息:
The web application [ROOT] appears to have started a thread named [pool-5-thread-2] but has failed to stop it. This is very likely to create a memory leak.
異常信息顯示: 應用程序啓動了一個名爲pool-5-thread-2的線程,但是關閉失敗,這很可能導致內存泄漏。
很明顯,tomcat沒能關掉ThreadPoolExecutor的核心線程,因此需要在關閉tomcat前手動關閉。
如何手動關閉線程池請參考:停止Tomcat webapp 線程池報錯的嘗試解決
那如果我不想手動關閉線程池怎麼辦?
使用Spring中中的 ThreadPoolTaskExecutor。將線程池的關閉交給Spring去管理。
在applicationContext.xml中加入
<!-- spring thread pool executor -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 線程池維護線程的最少數量 -->
<property name="corePoolSize" value="10"/>
<!-- 允許的空閒時間 -->
<property name="keepAliveSeconds" value="5"/>
<!-- 線程池維護線程的最大數量 -->
<property name="maxPoolSize" value="20"/>
<!-- 緩存隊列 -->
<property name="queueCapacity" value="4"/>
<!-- 對拒絕task的處理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
</bean>
程序中:
@Autowired
private ThreadPoolTaskExecutor threadPool;
threadPool.execute(() -> {
model.addAttribute("types", typeService.findHottestType(6));
countDownLatch.countDown();
});
threadPool.execute(() -> {
model.addAttribute("tags", tagService.findHottestTag(6));
countDownLatch.countDown();
});
threadPool.execute(() -> {
model.addAttribute("blogs", blogService.findHotestBlogs());
countDownLatch.countDown();
});
threadPool.execute(() -> {
model.addAttribute("newBlogs", blogService.findNewestBlogs(8));
countDownLatch.countDown();
});
再次啓動程序,測試,關閉tomcat,報錯信息不再提示。
如何優雅安全的關閉線程池?
線程池關閉不合理,導致應用⽆法正常stop的情況,這是我們很容易忽略的。
下面就以ThreadPoolExecutor爲例,來介紹下如何優雅的關閉線程池。
線程中斷
在介紹線程池關閉之前,先介紹下Thread的interrupt。
在程序中,我們是不能隨便中斷⼀個線程的,因爲這是極其不安全的操作,我們⽆法知道這個線程正運⾏在什麼狀態,它可能持有某把鎖,強⾏中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強⾏中斷導致數據不一致,從而混亂的問題。正因此,Java⾥將Thread的stop⽅法設置爲過時,以禁⽌⼤家使⽤。
⼀個線程什麼時候可以退出呢?當然只有線程自⼰才能知道。
所以我們這⾥要說的Thread的interrrupt⽅法,本質不是⽤來中斷一個線程。而是將線程設置⼀箇中斷狀態。當我們調⽤線程的interrupt方法,它有兩個作⽤用:
1、如果此線程處於阻塞狀態(比如調⽤了wait方法,io等待),則會立刻退出阻塞,並拋出InterruptedException異常,線程就可以通過捕獲InterruptedException來做⼀定的處理,然後讓線程退出。
2、如果此線程正處於運行之中,則線程不受任何影響,繼續運行,僅僅是線程的中斷標記被設置爲true。所以線程要在適當的位置通過調用isInterrupted方法來查看自⼰是否被中斷,並做退出操作。
注:如果線程的interrupt方法先被調用,然後線程調用阻塞方法進入阻塞狀態,InterruptedException異常依舊會拋出。如果線程捕獲InterruptedException異常後,繼續調用阻塞方法, 將不再觸發InterruptedException異常。
線程池的關閉
線程池提供了兩個關閉方法,shutdownNow和shuwdown⽅法。
shutdownNow⽅法的解釋是:線程池拒接收新提交的任務,同時⽴刻關閉線程池,線程池里的任務不再執行。
shutdown⽅法的解釋是:線程池拒接收新提交的任務,同時等待線程池⾥的任務執行完畢後關閉線程池。
以上的說法雖然沒錯,但是還有很多的細節問題,比如調用shutdown⽅法後,正在執⾏的任務的線程會做出什麼反應?正在等待任務的線程又會做出什麼反應?線程在什麼情況下才會徹底退出。如果不瞭解這些細節,在關閉線程池時就難免遇到,像線程池關閉不了,關閉線程池出現報錯等情況。
再說這些關閉線程池細節問題之前,需要強調一點的是,調用完shutdownNow和shuwdown⽅法後,並不代表線程池已經完成關閉操作,它只是異步的通知線程池進行關閉處理。如果要同步等待線程池徹底關閉後才繼續往下執行,需要調⽤awaitTermination⽅法進⾏同步等待。
我們來看看shutdownNow和shuwdown⽅法的源碼:
shutdown:
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
// 上鎖,確保同一時間只能有一個線程執行此操作
mainLock.lock();
try {
// 檢查方法調用方是否用權限關閉線程池以及中斷工作線程
checkShutdownAccess();
// 將線程池運行狀態設置爲SHUTDOWN
advanceRunState(SHUTDOWN);
// 中斷所有空閒線程
interruptIdleWorkers();
// 此方法在ThreadPoolExecutor中是空實現,具體實現在其子類ScheduledThreadPoolExecutor
// 中,用於取消延時任務。
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 嘗試將線程池置爲TERMINATED狀態
tryTerminate();
}
shutdownNow:
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
*
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
*
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
*
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 檢查方法調用方是否用權限關閉線程池以及中斷工作線程
checkShutdownAccess();
// 將線程池運行狀態置爲STOP
advanceRunState(STOP);
// 中斷所有線程,包括正在運行的線程
interruptWorkers();
// 將未執行的任務移入列表中
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 嘗試將線程池置爲TERMINATED狀態
tryTerminate();
return tasks;
}
具體源碼解釋請參考:關閉線程池的正確姿勢